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.
Turn it on
Section titled “Turn it on”- Open Settings (Cmd+,) and choose Editor.
- Under Live Sync, switch on Live Sync.
- 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.
How a game uses it
Section titled “How a game uses 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.
POST /sync
Section titled “POST /sync”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 }]}addedandchangedhold text files (.lostproj,.lostmap,.lostset,.lostentity,.lostanimand.json), with their bytes incontentas base64.addedmeans you didn’t have the file;changedmeans yours is out of date.imageslists the PNGs that are new or changed. Download each one withGET /resource.deletedlists files you have that the project no longer has.- Hashes are the lowercase hex SHA-256 of the file’s bytes.
projectHashcovers 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 /resource?path=…
Section titled “GET /resource?path=…”GET /api/v1/share/resource?path=resources/Ground.pngReturns 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.
GET /changes
Section titled “GET /changes”A WebSocket that sends plain text, not JSON:
readyas soon as you connect. Sync then, to pick up anything you missed while disconnected.changedeach 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.
A complete client
Section titled “A complete client”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.
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.
