@bitmagic/cli 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -2
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/edit.d.ts +10 -0
- package/dist/commands/edit.js +142 -0
- package/dist/commands/edit.js.map +1 -0
- package/dist/commands/upgrade.d.ts +10 -0
- package/dist/commands/upgrade.js +43 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/editor/save.d.ts +57 -0
- package/dist/editor/save.js +144 -0
- package/dist/editor/save.js.map +1 -0
- package/dist/editor/server.d.ts +17 -0
- package/dist/editor/server.js +185 -0
- package/dist/editor/server.js.map +1 -0
- package/dist/editor/shell-page.d.ts +38 -0
- package/dist/editor/shell-page.js +339 -0
- package/dist/editor/shell-page.js.map +1 -0
- package/dist/project/context.d.ts +2 -0
- package/dist/project/context.js.map +1 -1
- package/dist/scaffold/agents-md.d.ts +51 -0
- package/dist/scaffold/agents-md.js +102 -0
- package/dist/scaffold/agents-md.js.map +1 -0
- package/dist/scaffold/project-files.d.ts +8 -0
- package/dist/scaffold/project-files.js +19 -2
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/scaffold/project.js +6 -2
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/upgrade-project.d.ts +9 -2
- package/dist/scaffold/upgrade-project.js +16 -3
- package/dist/scaffold/upgrade-project.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface EditorServerOptions {
|
|
2
|
+
/** The project root — the directory holding `bitmagic.json`. */
|
|
3
|
+
root: string;
|
|
4
|
+
/** The game this project owns, from `bitmagic.json`. */
|
|
5
|
+
gameId: string;
|
|
6
|
+
/** Port vite serves the game on; the shell iframes it. */
|
|
7
|
+
gamePort: number;
|
|
8
|
+
/** Port to bind. Fails loudly if taken — the URL is meant to be stable and bookmarkable. */
|
|
9
|
+
port: number;
|
|
10
|
+
log?: (message: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface EditorServer {
|
|
13
|
+
/** The URL to open, e.g. `http://localhost:3011/`. */
|
|
14
|
+
readonly url: string;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function startEditorServer(options: EditorServerOptions): Promise<EditorServer>;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor sidecar: a loopback HTTP server that gives the shell page the two things a browser
|
|
3
|
+
* cannot do for itself — read the project's game data off disk, and write scene edits back to
|
|
4
|
+
* `src/work/world.json`.
|
|
5
|
+
*
|
|
6
|
+
* It is deliberately NOT part of the project's vite server. Vite serves `<projectRoot>` and its
|
|
7
|
+
* config is a file the creator owns (`vite.config.js`, scaffolded once and never upgraded), so
|
|
8
|
+
* putting a save endpoint there would freeze this protocol at whatever version the project was
|
|
9
|
+
* scaffolded with. Running it from the CLI instead means `bitmagic edit` ships its own shell and
|
|
10
|
+
* its own endpoints, and an old project gets the new editor by upgrading the CLI alone.
|
|
11
|
+
*
|
|
12
|
+
* Bound to 127.0.0.1 for the obvious reason: it writes files in the creator's project. It must not
|
|
13
|
+
* be reachable off the machine. CORS is answered explicitly because the shell (this origin) and the
|
|
14
|
+
* game (vite's origin) are different ports — only the shell talks to these endpoints, but the
|
|
15
|
+
* preflight has to succeed either way.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as http from 'http';
|
|
19
|
+
import { CliError } from '../errors.js';
|
|
20
|
+
import { applyModificationsToWorld } from '../forge/apply-modifications.js';
|
|
21
|
+
import { readProjectGameData } from '../forge/browser-host.js';
|
|
22
|
+
import { projectWorldJsonPath } from '../forge/run-pipeline.js';
|
|
23
|
+
import { renderEditorShell } from './shell-page.js';
|
|
24
|
+
import { buildSceneModifications } from './save.js';
|
|
25
|
+
/** Bodies are a scene snapshot, not an upload — a megabyte is already generous. */
|
|
26
|
+
const MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
27
|
+
function readBody(req) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let body = '';
|
|
30
|
+
req.on('data', (chunk) => {
|
|
31
|
+
body += String(chunk);
|
|
32
|
+
if (body.length > MAX_BODY_BYTES) {
|
|
33
|
+
reject(new Error('Request body too large'));
|
|
34
|
+
req.destroy();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
req.on('end', () => resolve(body));
|
|
38
|
+
req.on('error', reject);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function writeCors(res) {
|
|
42
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
43
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
44
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
45
|
+
}
|
|
46
|
+
function sendJson(res, status, body) {
|
|
47
|
+
res.writeHead(status, { 'Content-Type': 'application/json' }).end(JSON.stringify(body));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Modification time of world.json, or 0 when it cannot be read.
|
|
51
|
+
*
|
|
52
|
+
* This is how the shell tells the creator's own saves apart from an edit their agent made while
|
|
53
|
+
* the editor was open. The server records the mtime it produced on every write; a value that does
|
|
54
|
+
* not match is someone else's write, and the shell offers a reload rather than silently continuing
|
|
55
|
+
* against a scene that no longer matches the file.
|
|
56
|
+
*/
|
|
57
|
+
function worldMtimeMs(worldPath) {
|
|
58
|
+
try {
|
|
59
|
+
return fs.statSync(worldPath).mtimeMs;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function readWorld(worldPath) {
|
|
66
|
+
const parsed = JSON.parse(fs.readFileSync(worldPath, 'utf-8'));
|
|
67
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
68
|
+
throw new CliError(`${worldPath} does not contain a world object.`);
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
export async function startEditorServer(options) {
|
|
73
|
+
const log = options.log ?? (() => { });
|
|
74
|
+
const worldPath = projectWorldJsonPath(options.root);
|
|
75
|
+
const shell = renderEditorShell({ gamePort: options.gamePort, gameId: options.gameId });
|
|
76
|
+
/** The mtime our own last write produced. Anything else is an external edit. */
|
|
77
|
+
let lastWrittenMtimeMs = worldMtimeMs(worldPath);
|
|
78
|
+
const handleSave = async (req, res) => {
|
|
79
|
+
let payload;
|
|
80
|
+
try {
|
|
81
|
+
payload = JSON.parse(await readBody(req));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
sendJson(res, 400, { ok: false, error: 'Request body must be JSON' });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (payload?.status?.unlocked !== true) {
|
|
88
|
+
// Not an error: the engine reports the scene locked until the world has loaded, and the
|
|
89
|
+
// shell's poll can land in that window.
|
|
90
|
+
sendJson(res, 200, { ok: true, applied: 0, skipped: 'scene editing is locked' });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let modifications;
|
|
94
|
+
try {
|
|
95
|
+
modifications = buildSceneModifications(payload, readWorld(worldPath));
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
log(`[editor] could not read ${worldPath}: ${message}`);
|
|
100
|
+
sendJson(res, 500, { ok: false, error: message });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (modifications.length === 0) {
|
|
104
|
+
sendJson(res, 200, { ok: true, applied: 0, worldMtimeMs: lastWrittenMtimeMs });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const outcome = applyModificationsToWorld(worldPath, modifications);
|
|
109
|
+
lastWrittenMtimeMs = worldMtimeMs(worldPath);
|
|
110
|
+
for (const line of outcome.summary)
|
|
111
|
+
log(`[editor] ${line}`);
|
|
112
|
+
sendJson(res, 200, {
|
|
113
|
+
ok: true,
|
|
114
|
+
applied: outcome.applied,
|
|
115
|
+
summary: outcome.summary,
|
|
116
|
+
worldMtimeMs: lastWrittenMtimeMs,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// `applyModificationsToWorld` writes nothing when it throws, so world.json still holds
|
|
121
|
+
// whatever it had. Report it rather than swallowing: the shell surfaces it, because a save
|
|
122
|
+
// that silently did nothing is the worst outcome for an autosaving editor.
|
|
123
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
124
|
+
log(`[editor] save refused: ${message}`);
|
|
125
|
+
sendJson(res, 422, { ok: false, error: message });
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const server = http.createServer((req, res) => {
|
|
129
|
+
void (async () => {
|
|
130
|
+
writeCors(res);
|
|
131
|
+
if (req.method === 'OPTIONS') {
|
|
132
|
+
res.writeHead(204).end();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const url = (req.url ?? '/').split('?')[0];
|
|
136
|
+
if (req.method === 'GET' && (url === '/' || url === '/index.html')) {
|
|
137
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }).end(shell);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (req.method === 'GET' && url === '/api/game-data') {
|
|
141
|
+
try {
|
|
142
|
+
sendJson(res, 200, {
|
|
143
|
+
gameId: options.gameId,
|
|
144
|
+
// The same merge the forge browser uses: `GameEngine.loadGame` reads
|
|
145
|
+
// `worldProfileData` from world.json and `gameGenre` from game.json, and refuses to
|
|
146
|
+
// load with either missing.
|
|
147
|
+
gameData: readProjectGameData(options.root, { gameId: options.gameId }),
|
|
148
|
+
worldMtimeMs: worldMtimeMs(worldPath),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
+
sendJson(res, 500, { ok: false, error: message });
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (req.method === 'GET' && url === '/api/state') {
|
|
158
|
+
sendJson(res, 200, {
|
|
159
|
+
worldMtimeMs: worldMtimeMs(worldPath),
|
|
160
|
+
lastWrittenMtimeMs,
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (req.method === 'POST' && url === '/api/scene/save') {
|
|
165
|
+
await handleSave(req, res);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
sendJson(res, 404, { ok: false, error: `No route for ${req.method ?? '?'} ${url}` });
|
|
169
|
+
})();
|
|
170
|
+
});
|
|
171
|
+
await new Promise((resolve, reject) => {
|
|
172
|
+
server.once('error', (error) => {
|
|
173
|
+
reject(error.code === 'EADDRINUSE'
|
|
174
|
+
? new CliError(`Port ${options.port} is already in use, so the editor cannot start. `
|
|
175
|
+
+ 'Stop whatever is using it, or pass `--editor-port`.')
|
|
176
|
+
: new CliError(`The editor server could not start: ${error.message}`));
|
|
177
|
+
});
|
|
178
|
+
server.listen(options.port, '127.0.0.1', resolve);
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
url: `http://localhost:${options.port}/`,
|
|
182
|
+
close: () => new Promise((resolve) => server.close(() => resolve())),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/editor/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor shell: the page that plays the Creator's role for a CLI-lane project.
|
|
3
|
+
*
|
|
4
|
+
* The whole feature rests on one fact — the visual editor is ALREADY in every scaffolded project.
|
|
5
|
+
* `VENDORED_DIRS` in `scaffold/project.ts` ships `engine/editor/` and `engine/debug/`, and
|
|
6
|
+
* `GameEngine` constructs `EditorManager` unconditionally, building the transform gizmo, the object
|
|
7
|
+
* inspector and the scene hierarchy into a hidden `#debug-container`. What the pro lane lacked was
|
|
8
|
+
* a parent frame speaking the Creator's `postMessage` protocol. This page is that frame, and
|
|
9
|
+
* nothing under `game/` changes to support it.
|
|
10
|
+
*
|
|
11
|
+
* Rendered from TypeScript rather than shipped as a `.html` asset because the package builds with
|
|
12
|
+
* plain `tsc`, which copies no static files. `smoke/harness.ts` and `scaffold/project-files.ts`
|
|
13
|
+
* both do the same.
|
|
14
|
+
*
|
|
15
|
+
* ── Four things that fail silently if changed ────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* 1. `?source=creator` on the iframe URL. Without it `isCreatorMode` is false
|
|
18
|
+
* (`game/src/engine/CreatorMode.ts`), the engine never registers its message listener, and
|
|
19
|
+
* every message below is discarded with no error anywhere.
|
|
20
|
+
* 2. `GAME_TEMPLATE_READY` must arrive before `LOAD_GAME` is posted. The engine registers its
|
|
21
|
+
* listener only after `await initI18n()`, and a `LOAD_GAME` landing before that is DROPPED,
|
|
22
|
+
* not queued — the symptom is a game that never loads, pointing at the wrong culprit.
|
|
23
|
+
* 3. `REQUEST_ASSETS` / `ADD_OBJECT` / `MARK_OBJECT_MODIFIED` use a FLAT envelope
|
|
24
|
+
* (`{ type, assets }`), not the `{ type, data }` one the rest of the protocol uses. That is
|
|
25
|
+
* the Creator's existing shape (`useIframeMessages.ts:1149`, `:332`) and the engine reads the
|
|
26
|
+
* fields off the message directly.
|
|
27
|
+
* 4. The autosave poll, rather than an event. `TransformControlsManager`'s mouseUp reaches
|
|
28
|
+
* `EditorManager.commitTransformChange()`, which only mutates a `Set` — the engine posts
|
|
29
|
+
* nothing. Polling `CHECK_SCENE_CHANGES` is what the Creator does too, just on tab switch
|
|
30
|
+
* instead of on a timer, and it covers drags, deletes, adds and inspector edits with one path.
|
|
31
|
+
*/
|
|
32
|
+
export interface EditorShellOptions {
|
|
33
|
+
/** Port vite serves the game on. */
|
|
34
|
+
gamePort: number;
|
|
35
|
+
/** The game this project owns. */
|
|
36
|
+
gameId: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function renderEditorShell(options: EditorShellOptions): string;
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor shell: the page that plays the Creator's role for a CLI-lane project.
|
|
3
|
+
*
|
|
4
|
+
* The whole feature rests on one fact — the visual editor is ALREADY in every scaffolded project.
|
|
5
|
+
* `VENDORED_DIRS` in `scaffold/project.ts` ships `engine/editor/` and `engine/debug/`, and
|
|
6
|
+
* `GameEngine` constructs `EditorManager` unconditionally, building the transform gizmo, the object
|
|
7
|
+
* inspector and the scene hierarchy into a hidden `#debug-container`. What the pro lane lacked was
|
|
8
|
+
* a parent frame speaking the Creator's `postMessage` protocol. This page is that frame, and
|
|
9
|
+
* nothing under `game/` changes to support it.
|
|
10
|
+
*
|
|
11
|
+
* Rendered from TypeScript rather than shipped as a `.html` asset because the package builds with
|
|
12
|
+
* plain `tsc`, which copies no static files. `smoke/harness.ts` and `scaffold/project-files.ts`
|
|
13
|
+
* both do the same.
|
|
14
|
+
*
|
|
15
|
+
* ── Four things that fail silently if changed ────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* 1. `?source=creator` on the iframe URL. Without it `isCreatorMode` is false
|
|
18
|
+
* (`game/src/engine/CreatorMode.ts`), the engine never registers its message listener, and
|
|
19
|
+
* every message below is discarded with no error anywhere.
|
|
20
|
+
* 2. `GAME_TEMPLATE_READY` must arrive before `LOAD_GAME` is posted. The engine registers its
|
|
21
|
+
* listener only after `await initI18n()`, and a `LOAD_GAME` landing before that is DROPPED,
|
|
22
|
+
* not queued — the symptom is a game that never loads, pointing at the wrong culprit.
|
|
23
|
+
* 3. `REQUEST_ASSETS` / `ADD_OBJECT` / `MARK_OBJECT_MODIFIED` use a FLAT envelope
|
|
24
|
+
* (`{ type, assets }`), not the `{ type, data }` one the rest of the protocol uses. That is
|
|
25
|
+
* the Creator's existing shape (`useIframeMessages.ts:1149`, `:332`) and the engine reads the
|
|
26
|
+
* fields off the message directly.
|
|
27
|
+
* 4. The autosave poll, rather than an event. `TransformControlsManager`'s mouseUp reaches
|
|
28
|
+
* `EditorManager.commitTransformChange()`, which only mutates a `Set` — the engine posts
|
|
29
|
+
* nothing. Polling `CHECK_SCENE_CHANGES` is what the Creator does too, just on tab switch
|
|
30
|
+
* instead of on a timer, and it covers drags, deletes, adds and inspector edits with one path.
|
|
31
|
+
*/
|
|
32
|
+
/** How often the shell asks the engine whether anything changed. A `Set` read; effectively free. */
|
|
33
|
+
const POLL_INTERVAL_MS = 500;
|
|
34
|
+
/** Attribute-safe. `gameId` comes from `bitmagic.json`, but it lands inside an HTML attribute. */
|
|
35
|
+
function escapeAttr(value) {
|
|
36
|
+
return value
|
|
37
|
+
.replace(/&/g, '&')
|
|
38
|
+
.replace(/"/g, '"')
|
|
39
|
+
.replace(/</g, '<')
|
|
40
|
+
.replace(/>/g, '>');
|
|
41
|
+
}
|
|
42
|
+
export function renderEditorShell(options) {
|
|
43
|
+
const gameUrl = `http://localhost:${options.gamePort}/?source=creator&gameId=${encodeURIComponent(options.gameId)}`;
|
|
44
|
+
return `<!doctype html>
|
|
45
|
+
<html lang="en">
|
|
46
|
+
<head>
|
|
47
|
+
<meta charset="utf-8">
|
|
48
|
+
<title>bitmagic edit — ${escapeAttr(options.gameId)}</title>
|
|
49
|
+
<style>
|
|
50
|
+
:root { color-scheme: dark; }
|
|
51
|
+
* { box-sizing: border-box; }
|
|
52
|
+
/* Flex column rather than calc() heights: the banners come and go, and two of them at once
|
|
53
|
+
must not push the viewport into a scrollbar. */
|
|
54
|
+
body { margin: 0; height: 100vh; display: flex; flex-direction: column;
|
|
55
|
+
background: #101014; color: #e6e6ea; font: 13px/1.45 ui-sans-serif, system-ui, sans-serif; }
|
|
56
|
+
#bar { display: flex; align-items: center; gap: 12px; height: 36px; flex: none; padding: 0 12px;
|
|
57
|
+
background: #17171d; border-bottom: 1px solid #26262e; }
|
|
58
|
+
#bar .id { color: #8a8a99; font-family: ui-monospace, monospace; }
|
|
59
|
+
#bar .spacer { flex: 1; }
|
|
60
|
+
#status { display: flex; align-items: center; gap: 6px; }
|
|
61
|
+
#dot { width: 8px; height: 8px; border-radius: 50%; background: #4a4a57; }
|
|
62
|
+
#dot.saving { background: #d9a441; }
|
|
63
|
+
#dot.saved { background: #4caf72; }
|
|
64
|
+
#dot.error { background: #e0564f; }
|
|
65
|
+
button { background: #26262e; color: #e6e6ea; border: 1px solid #34343f; border-radius: 5px;
|
|
66
|
+
padding: 4px 10px; font: inherit; cursor: pointer; }
|
|
67
|
+
button:hover { background: #30303a; }
|
|
68
|
+
.banner { display: none; align-items: center; gap: 12px; flex: none; padding: 8px 12px;
|
|
69
|
+
background: #3a2c12; border-bottom: 1px solid #574018; color: #f0d9a8; }
|
|
70
|
+
.banner.show { display: flex; }
|
|
71
|
+
#terrain-banner { background: #3d1f1c; border-bottom-color: #6b2f28; color: #f3c3bd; }
|
|
72
|
+
#frame { display: block; flex: 1; width: 100%; border: 0; min-height: 0; }
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
<div id="bar">
|
|
77
|
+
<strong>bitmagic edit</strong>
|
|
78
|
+
<span class="id">${escapeAttr(options.gameId)}</span>
|
|
79
|
+
<span class="spacer"></span>
|
|
80
|
+
<span id="status"><span id="dot"></span><span id="status-text">Loading…</span></span>
|
|
81
|
+
<button id="reload" type="button">Reload</button>
|
|
82
|
+
</div>
|
|
83
|
+
<div id="banner" class="banner">
|
|
84
|
+
<span>src/work/world.json changed on disk — this scene is out of date.</span>
|
|
85
|
+
<button id="banner-reload" type="button">Reload scene</button>
|
|
86
|
+
</div>
|
|
87
|
+
<div id="terrain-banner" class="banner">
|
|
88
|
+
<span><strong>Terrain edits are not saved yet.</strong>
|
|
89
|
+
Sculpting the ground needs an asset upload that <code>bitmagic edit</code> does not do — these
|
|
90
|
+
voxel changes will be lost. Discard them, or press Cancel in the terrain toolbar.</span>
|
|
91
|
+
<button id="terrain-discard" type="button">Discard terrain edits</button>
|
|
92
|
+
</div>
|
|
93
|
+
<iframe id="frame" allow="autoplay; fullscreen; xr-spatial-tracking; clipboard-write"></iframe>
|
|
94
|
+
<script>
|
|
95
|
+
(function () {
|
|
96
|
+
'use strict';
|
|
97
|
+
var GAME_URL = ${JSON.stringify(gameUrl)};
|
|
98
|
+
var POLL_MS = ${POLL_INTERVAL_MS};
|
|
99
|
+
|
|
100
|
+
var frame = document.getElementById('frame');
|
|
101
|
+
var dot = document.getElementById('dot');
|
|
102
|
+
var statusText = document.getElementById('status-text');
|
|
103
|
+
var banner = document.getElementById('banner');
|
|
104
|
+
var terrainBanner = document.getElementById('terrain-banner');
|
|
105
|
+
|
|
106
|
+
var loaded = false;
|
|
107
|
+
var saving = false;
|
|
108
|
+
var markersDirty = false;
|
|
109
|
+
var lastKnownMtime = 0;
|
|
110
|
+
var pollTimer = null;
|
|
111
|
+
var waiters = [];
|
|
112
|
+
// Bumped by reload(). Every async step checks it, so a reload triggered mid-boot cannot leave
|
|
113
|
+
// two polling loops running against one iframe.
|
|
114
|
+
var generation = 0;
|
|
115
|
+
|
|
116
|
+
function setStatus(kind, text) {
|
|
117
|
+
dot.className = kind || '';
|
|
118
|
+
statusText.textContent = text;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function post(type, data) {
|
|
122
|
+
if (frame.contentWindow) frame.contentWindow.postMessage({ type: type, data: data }, '*');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The flat envelope — see note 3 in this file's header.
|
|
126
|
+
function postFlat(message) {
|
|
127
|
+
if (frame.contentWindow) frame.contentWindow.postMessage(message, '*');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Resolve on the next message of this type, or null after timeoutMs. Never rejects. */
|
|
131
|
+
function await_(type, timeoutMs) {
|
|
132
|
+
return new Promise(function (resolve) {
|
|
133
|
+
var waiter = { type: type, resolve: resolve, timer: null };
|
|
134
|
+
waiter.timer = setTimeout(function () {
|
|
135
|
+
var i = waiters.indexOf(waiter);
|
|
136
|
+
if (i !== -1) waiters.splice(i, 1);
|
|
137
|
+
resolve(null);
|
|
138
|
+
}, timeoutMs);
|
|
139
|
+
waiters.push(waiter);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
window.addEventListener('message', function (event) {
|
|
144
|
+
var message = event.data;
|
|
145
|
+
if (!message || typeof message.type !== 'string') return;
|
|
146
|
+
|
|
147
|
+
for (var i = waiters.length - 1; i >= 0; i--) {
|
|
148
|
+
if (waiters[i].type === message.type) {
|
|
149
|
+
clearTimeout(waiters[i].timer);
|
|
150
|
+
waiters[i].resolve(message);
|
|
151
|
+
waiters.splice(i, 1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
switch (message.type) {
|
|
156
|
+
case 'REQUEST_ASSETS':
|
|
157
|
+
// Re-read rather than serving the copy captured at boot: an agent may have generated an
|
|
158
|
+
// asset since this page loaded, and a stale list is an empty asset palette with no
|
|
159
|
+
// explanation.
|
|
160
|
+
fetch('/api/game-data').then(function (r) { return r.json(); }).then(function (payload) {
|
|
161
|
+
var data = payload && payload.gameData;
|
|
162
|
+
postFlat({ type: 'ASSETS_RESPONSE', assets: (data && data.assets) || [] });
|
|
163
|
+
}).catch(function () {
|
|
164
|
+
postFlat({ type: 'ASSETS_RESPONSE', assets: [] });
|
|
165
|
+
});
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
case 'ADD_OBJECT':
|
|
169
|
+
// The engine already placed the object; all the host owes it is a dirty mark so the next
|
|
170
|
+
// poll picks the new id up. (The Creator also sets environmentObjectsManuallyEdited here —
|
|
171
|
+
// that flag exists only to gate the web lane's world-edit CLI and has no meaning for a
|
|
172
|
+
// project whose agent edits world.json directly.)
|
|
173
|
+
if (message.objectId) postFlat({ type: 'MARK_OBJECT_MODIFIED', objectId: message.objectId });
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case 'ADD_MARKER':
|
|
177
|
+
case 'UPDATE_MARKER':
|
|
178
|
+
markersDirty = true;
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
// No other tabs exist here, and asset re-baking is a "bitmagic generate" concern.
|
|
182
|
+
case 'SWITCH_TO_TAB':
|
|
183
|
+
case 'OPEN_ASSET_ACTION':
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
async function boot(gen) {
|
|
189
|
+
loaded = false;
|
|
190
|
+
markersDirty = false;
|
|
191
|
+
banner.classList.remove('show');
|
|
192
|
+
terrainBanner.classList.remove('show');
|
|
193
|
+
setStatus('', 'Loading…');
|
|
194
|
+
|
|
195
|
+
var response = await fetch('/api/game-data');
|
|
196
|
+
if (gen !== generation) return;
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
setStatus('error', 'Could not read the project');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
var project = await response.json();
|
|
202
|
+
lastKnownMtime = project.worldMtimeMs || 0;
|
|
203
|
+
|
|
204
|
+
// Both waits are armed BEFORE navigation: either signal can arrive while the page loads.
|
|
205
|
+
var templateReady = await_('GAME_TEMPLATE_READY', 60000);
|
|
206
|
+
var gameLoaded = await_('GAME_LOADED', 120000);
|
|
207
|
+
frame.src = GAME_URL;
|
|
208
|
+
|
|
209
|
+
var ready = await templateReady;
|
|
210
|
+
if (gen !== generation) return;
|
|
211
|
+
if (!ready) {
|
|
212
|
+
setStatus('error', 'The game never finished booting — try "bitmagic verify"');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
post('LOAD_GAME', { gameId: project.gameId, gameData: project.gameData, skipMenu: true });
|
|
216
|
+
var started = await gameLoaded;
|
|
217
|
+
if (gen !== generation) return;
|
|
218
|
+
if (!started) {
|
|
219
|
+
setStatus('error', 'The world never loaded — try "bitmagic verify"');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Mirrors the Creator's enterEditorMode(): pause, turn the editor on (this is what unpacks
|
|
224
|
+
// InstancedMeshes so clicks can hit an individual instance), then configure the free camera
|
|
225
|
+
// and hide the HUD.
|
|
226
|
+
post('SET_PAUSE', { paused: true });
|
|
227
|
+
post('SET_EDITOR_MODE', { enabled: true });
|
|
228
|
+
post('SET_EDITOR_TAB', { tab: 'scene' });
|
|
229
|
+
|
|
230
|
+
loaded = true;
|
|
231
|
+
setStatus('saved', 'Ready');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function flush() {
|
|
235
|
+
if (!loaded || saving) return;
|
|
236
|
+
|
|
237
|
+
post('CHECK_SCENE_CHANGES');
|
|
238
|
+
var changes = await await_('SCENE_HAS_CHANGES', 2000);
|
|
239
|
+
if (!changes) return;
|
|
240
|
+
if (!changes.hasChanges && !markersDirty) return;
|
|
241
|
+
|
|
242
|
+
post('GET_SCENE_EDITING_STATUS');
|
|
243
|
+
var status = await await_('SCENE_EDITING_STATUS', 5000);
|
|
244
|
+
if (!status) return;
|
|
245
|
+
|
|
246
|
+
saving = true;
|
|
247
|
+
setStatus('saving', 'Saving…');
|
|
248
|
+
try {
|
|
249
|
+
var response = await fetch('/api/scene/save', {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers: { 'Content-Type': 'application/json' },
|
|
252
|
+
body: JSON.stringify({ changes: changes, status: status, markersDirty: markersDirty })
|
|
253
|
+
});
|
|
254
|
+
var result = await response.json();
|
|
255
|
+
if (!response.ok || !result.ok) {
|
|
256
|
+
setStatus('error', result.error || 'Save failed');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
lastKnownMtime = result.worldMtimeMs || lastKnownMtime;
|
|
260
|
+
markersDirty = false;
|
|
261
|
+
// Only once the write landed: clearing earlier would drop the edit on a failed save.
|
|
262
|
+
post('CLEAR_SCENE_CHANGES');
|
|
263
|
+
setStatus('saved', result.applied > 0 ? 'Saved to world.json' : 'Ready');
|
|
264
|
+
} catch (error) {
|
|
265
|
+
setStatus('error', 'Save failed: ' + error.message);
|
|
266
|
+
} finally {
|
|
267
|
+
saving = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Terrain sculpting is out of scope for this command, but it is not out of REACH: clicking the
|
|
273
|
+
* ground drops the engine straight into a whole-terrain voxel session (EditorManager.selectObject
|
|
274
|
+
* routes every terrain-chunk hit to startTerrainVoxelSession, with no lock to stop it). Its
|
|
275
|
+
* "Save & Exit" uploads the terrain VXL to a signed URL that only the web lane and the forge can
|
|
276
|
+
* mint — here saveToS3 fails, logs a console warning, and marks the session committed. The
|
|
277
|
+
* creator would lose the work with no visible sign.
|
|
278
|
+
*
|
|
279
|
+
* So say so, within one poll of the first voxel edit, and offer the revert the engine already
|
|
280
|
+
* implements. hasUnsavedTerrainChanges() is false for a session that was merely opened, so
|
|
281
|
+
* clicking the ground by accident stays silent.
|
|
282
|
+
*/
|
|
283
|
+
async function checkTerrainEdits() {
|
|
284
|
+
if (!loaded) return;
|
|
285
|
+
post('CHECK_TERRAIN_CHANGES');
|
|
286
|
+
var terrain = await await_('TERRAIN_HAS_CHANGES', 2000);
|
|
287
|
+
if (!terrain) return;
|
|
288
|
+
terrainBanner.classList.toggle('show', terrain.hasChanges === true);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function checkExternalEdits() {
|
|
292
|
+
if (!loaded || saving) return;
|
|
293
|
+
try {
|
|
294
|
+
var state = await (await fetch('/api/state')).json();
|
|
295
|
+
// Anything other than the mtime our own last write produced is someone else's edit — most
|
|
296
|
+
// likely the creator's agent. Offer a reload rather than taking one: an unprompted reload
|
|
297
|
+
// would discard a drag in progress.
|
|
298
|
+
if (state.worldMtimeMs && state.worldMtimeMs !== lastKnownMtime
|
|
299
|
+
&& state.worldMtimeMs !== state.lastWrittenMtimeMs) {
|
|
300
|
+
banner.classList.add('show');
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
// The sidecar is this page's own server; if it is gone the reload button is the only
|
|
304
|
+
// meaningful action left, and the poll below will keep trying.
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// One serial loop rather than two timers: a flush and an mtime check must never overlap, or the
|
|
309
|
+
// check reads the mtime of a write that has not finished being accounted for.
|
|
310
|
+
async function loop(gen) {
|
|
311
|
+
if (gen !== generation) return;
|
|
312
|
+
await flush();
|
|
313
|
+
await checkTerrainEdits();
|
|
314
|
+
await checkExternalEdits();
|
|
315
|
+
if (gen !== generation) return;
|
|
316
|
+
pollTimer = setTimeout(function () { loop(gen); }, POLL_MS);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function start() {
|
|
320
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
321
|
+
var gen = ++generation;
|
|
322
|
+
boot(gen).then(function () { loop(gen); });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
document.getElementById('reload').addEventListener('click', start);
|
|
326
|
+
document.getElementById('banner-reload').addEventListener('click', start);
|
|
327
|
+
document.getElementById('terrain-discard').addEventListener('click', function () {
|
|
328
|
+
post('REVERT_TERRAIN_CHANGES');
|
|
329
|
+
terrainBanner.classList.remove('show');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
start();
|
|
333
|
+
})();
|
|
334
|
+
</script>
|
|
335
|
+
</body>
|
|
336
|
+
</html>
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=shell-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-page.js","sourceRoot":"","sources":["../../src/editor/shell-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,oGAAoG;AACpG,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B,kGAAkG;AAClG,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AASD,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,OAAO,GAAG,oBAAoB,OAAO,CAAC,QAAQ,2BAA2B,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IACpH,OAAO;;;;yBAIgB,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBA8B5B,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;mBAmB9B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;kBACxB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+OjC,CAAC;AACF,CAAC"}
|
|
@@ -3,6 +3,8 @@ export interface ProjectMetadata {
|
|
|
3
3
|
engineVersion: string;
|
|
4
4
|
genre: string;
|
|
5
5
|
template: string;
|
|
6
|
+
/** See the same field in scaffold/project-files.ts. Absent on projects scaffolded before it. */
|
|
7
|
+
agentsMdHash?: string;
|
|
6
8
|
}
|
|
7
9
|
export interface ProjectContext {
|
|
8
10
|
root: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/project/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/project/context.ts"],"names":[],"mappings":"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"}
|