Skip to content

Live Sync

Live Sync lets a game you are developing load its content straight from the editor, and reload it a moment after you change something. The editor serves the same files that Export Project writes (see Project export format) over HTTP, on your own computer.

  1. Open Settings (Cmd+,) and choose Editor.
  2. Under Live Sync, switch on Live Sync.
  3. To have it on every time, also switch on Start when a project opens.

Live Sync runs while a project is open. It listens on port 7788 unless you change Port, and only on 127.0.0.1, so only programs on the same computer can reach it.

All addresses start with http://127.0.0.1:7788/api/v1/share.

Request What it does
POST /sync You send the files you have; the editor answers with what changed.
GET /resource Downloads one image.
GET /changes A WebSocket. The editor says when the project changed.

Your game keeps its own copy of the files, plus a list of each file’s hash. When the editor says something changed, the game syncs: it sends its list, downloads the images that changed, writes the changed files, deletes the removed ones and reloads.

Send the files you have, as path and hash. An empty files gets you everything.

{ "files": { "Map_1.lostproj": "761c0826…", "resources/Ground.png": "c91438b6…" } }

The answer lists only what differs from your copy:

{
"projectId": "7aef6bc9cbd2416ebb890e53a4d8c047",
"projectName": "My Game",
"projectHash": "ae8dcd407d7e…",
"added": [{ "path": "maps/Map_1.lostmap", "hash": "4c407b1b…", "content": "ewogICJjb2xs…" }],
"changed": [],
"deleted": ["entities/Old_Chest.lostentity"],
"images": [{ "path": "resources/Ground.png", "hash": "c91438b6…", "size": 276 }]
}
  • added and changed hold text files (.lostproj, .lostmap, .lostset, .lostentity, .lostanim and .json), with their bytes in content as base64. added means you didn’t have the file; changed means yours is out of date.
  • images lists the PNGs that are new or changed. Download each one with GET /resource.
  • deleted lists files you have that the project no longer has.
  • Hashes are the lowercase hex SHA-256 of the file’s bytes. projectHash covers the whole project: if it matches the last one you saw, nothing changed.

To make sure the game gets the project it expects, add "project": "<project id>". If another project is open, the editor answers 409 with its projectId and projectName. A 503 means the export failed and there is nothing to serve; the editor shows why.

GET /api/v1/share/resource?path=resources/Ground.png

Returns the file’s bytes, with the hash as its ETag. Send If-None-Match with a hash you already have and you get 304 Not Modified instead.

A WebSocket that sends plain text, not JSON:

  • ready as soon as you connect. Sync then, to pick up anything you missed while disconnected.
  • changed each time the project changes. It is sent for every edit, so wait for the edits to pause before you sync. An edit that doesn’t change any exported file gives an empty sync.

This Node.js 22 script keeps a content folder in step with the open project. It uses the built-in fetch and WebSocket, so it needs no packages. Run it with node live-sync.mjs, edit something in the editor, and the folder updates.

live-sync.mjs
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
const BASE = 'http://127.0.0.1:7788/api/v1/share';
const DIR = path.resolve('content');
const MANIFEST = path.join(DIR, '.manifest.json');
// Paths come from the editor; refuse any that would land outside DIR.
function target(rel) {
const file = path.resolve(DIR, rel);
if (!file.startsWith(DIR + path.sep)) throw new Error(`refusing path ${rel}`);
return file;
}
async function write(rel, bytes) {
const file = target(rel);
await mkdir(path.dirname(file), { recursive: true });
await writeFile(file, bytes);
}
async function sync() {
let files = {};
try {
files = JSON.parse(await readFile(MANIFEST, 'utf8'));
} catch {}
const res = await fetch(`${BASE}/sync`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ files }),
});
if (!res.ok) throw new Error(`sync failed: ${res.status}`);
const delta = await res.json();
// Images first, so no map or tileset points at a picture we don't have yet.
for (const image of delta.images) {
const r = await fetch(`${BASE}/resource?path=${encodeURIComponent(image.path)}`);
if (!r.ok) throw new Error(`${image.path}: ${r.status}`);
await write(image.path, Buffer.from(await r.arrayBuffer()));
files[image.path] = r.headers.get('etag').replaceAll('"', '');
}
for (const file of [...delta.added, ...delta.changed]) {
await write(file.path, Buffer.from(file.content, 'base64'));
files[file.path] = file.hash;
}
for (const rel of delta.deleted) {
await rm(target(rel), { force: true });
delete files[rel];
}
await mkdir(DIR, { recursive: true });
await writeFile(MANIFEST, JSON.stringify(files, null, 2));
const count = delta.images.length + delta.added.length + delta.changed.length + delta.deleted.length;
console.log(`${delta.projectName}: ${count} file(s) updated`);
// Reload your game's content here.
}
// Sync on "ready" and on every "changed", once the edits have paused for 300 ms.
function listen() {
const ws = new WebSocket(`${BASE.replace('http', 'ws')}/changes`);
let timer;
ws.onmessage = () => {
clearTimeout(timer);
timer = setTimeout(() => sync().catch((e) => console.error(e.message)), 300);
};
// The editor may be closed or Live Sync off: keep the files you have and try again later.
ws.onclose = () => setTimeout(listen, 2000);
}
listen();

The same steps work in any language with an HTTP client and a WebSocket client.

  • Treat an editor that isn’t running as normal: keep using the files you already have.
  • Only load from Live Sync in development builds. A release build should load exported files.
  • Write images before the files that refer to them, as the script does, so a reload never finds a missing picture.
  • If two parts of your game sync on their own, share one sync between them. Each sync uses up the changes it reports, so a second one right after it comes back empty.