@bitmagic/cli 0.1.18 → 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 +32 -0
- 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/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/scaffold/project-files.js +12 -0
- package/dist/scaffold/project-files.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ bitmagic publish # verify-gate, build, and ship — private by defa
|
|
|
35
35
|
| `bitmagic init [dir] [--template <id>] [--name <name>] [--idea "<pitch>"] [--env <env>] [--force]` | Scaffold a new project: mints a game, downloads the vendored engine, writes `AGENTS.md`, `GAME-DESIGN.md` and templates. `--idea` seeds the design doc's pitch. |
|
|
36
36
|
| `bitmagic check` | Typecheck (`tsc --noEmit`) against the vendored engine. Fast; does not prove the game runs. |
|
|
37
37
|
| `bitmagic dev [--port <port>]` | Build, then serve with `tsc --watch` + `vite`, default port 3010. |
|
|
38
|
+
| `bitmagic edit [--port <port>] [--editor-port <port>]` | Everything `dev` does, plus a visual scene editor at `http://localhost:3011/`: click an object, drag the gizmo, and the new transform is written to `src/work/world.json`. See **Editing the scene visually** below. |
|
|
38
39
|
| `bitmagic verify [--timeout <ms>]` | Build, boot the game in a headless browser, click the engine's Play button like a player would, and report what broke. A game without the button passes as long as gameplay starts by itself (auto-starting genres, custom start UIs); a run where gameplay never starts fails. `--timeout` is settle time in ms measured after gameplay starts, so the screenshot captures gameplay. Writes the artifacts the publish gate reads (see below). |
|
|
39
40
|
| `bitmagic build` | Bundle the game into one self-contained `.bitmagic/build/index.html`. Rarely needs to be run by hand — `publish` builds automatically when the project changed. |
|
|
40
41
|
| `bitmagic publish [--visibility public\|private] [--name <name>] [--description <desc>] [--force]` | Verify-gate, build if needed, and upload straight to GCS. **Private unless `--visibility public` is passed.** |
|
|
@@ -88,6 +89,37 @@ Re-running on an unchanged design generates nothing and costs nothing — it rep
|
|
|
88
89
|
cover. Edit the design and run again, or pass `--force`, to replace it. Because it writes
|
|
89
90
|
`world.json`, it invalidates a passing verify: run it before `bitmagic verify`, not after.
|
|
90
91
|
|
|
92
|
+
## Editing the scene visually
|
|
93
|
+
|
|
94
|
+
Some things are faster with a mouse. `bitmagic edit` runs everything `bitmagic dev` runs and adds a
|
|
95
|
+
local editor on a second port:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
bitmagic edit
|
|
99
|
+
# Serving the game at http://localhost:3010/
|
|
100
|
+
# Editor ready at http://localhost:3011/
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Open the editor URL in your normal browser. The game loads paused, with a free-flying camera, the
|
|
104
|
+
scene hierarchy and the transform gizmo — the same editor the web Creator uses, which already ships
|
|
105
|
+
inside `engine/`. Click an object to select it, then move, rotate, scale, snap it to the ground,
|
|
106
|
+
edit its numbers in the inspector, delete it, or right-click to add one from the project's assets.
|
|
107
|
+
|
|
108
|
+
**Every change is saved to `src/work/world.json` as you make it.** There is no Save button and no
|
|
109
|
+
unsaved state: your agent reads the same file, and `git diff` is the record of what you moved.
|
|
110
|
+
Saves are surgical — one entry per object touched, nothing else in the file is rewritten.
|
|
111
|
+
|
|
112
|
+
If your agent edits `world.json` while the editor is open, the editor says so and offers a reload
|
|
113
|
+
rather than silently reloading over work in progress.
|
|
114
|
+
|
|
115
|
+
Both ports must fall between 3000 and 3199. That is the window the asset CDN allows as an origin;
|
|
116
|
+
outside it the game loads with no terrain and no error.
|
|
117
|
+
|
|
118
|
+
What it does **not** do yet: terrain and voxel sculpting. Clicking the ground still opens the
|
|
119
|
+
engine's terrain tools, but saving them needs an asset upload this command does not perform, so the
|
|
120
|
+
editor warns you as soon as you change a voxel and offers to discard it. Use the web Creator for
|
|
121
|
+
terrain work.
|
|
122
|
+
|
|
91
123
|
## Publishing
|
|
92
124
|
|
|
93
125
|
`bitmagic publish` is two gated steps in one command:
|
package/dist/cli.d.ts
CHANGED
|
@@ -95,6 +95,16 @@ declare const rawCommands: {
|
|
|
95
95
|
readonly description: "Port to serve on (default 3010).";
|
|
96
96
|
};
|
|
97
97
|
}>;
|
|
98
|
+
edit: CommandDef<{
|
|
99
|
+
readonly port: {
|
|
100
|
+
readonly type: "string";
|
|
101
|
+
readonly description: "Port to serve the game on (default 3010).";
|
|
102
|
+
};
|
|
103
|
+
readonly 'editor-port': {
|
|
104
|
+
readonly type: "string";
|
|
105
|
+
readonly description: "Port to serve the editor on (default 3011).";
|
|
106
|
+
};
|
|
107
|
+
}>;
|
|
98
108
|
verify: CommandDef<{
|
|
99
109
|
readonly timeout: {
|
|
100
110
|
readonly type: "string";
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { initCommand } from './commands/init.js';
|
|
|
8
8
|
import { upgradeCommand } from './commands/upgrade.js';
|
|
9
9
|
import { checkCommand } from './commands/check.js';
|
|
10
10
|
import { devCommand } from './commands/dev.js';
|
|
11
|
+
import { editCommand } from './commands/edit.js';
|
|
11
12
|
import { verifyCommand } from './commands/verify.js';
|
|
12
13
|
import { buildCommand } from './commands/build.js';
|
|
13
14
|
import { generateCommand } from './commands/generate.js';
|
|
@@ -102,6 +103,7 @@ const rawCommands = {
|
|
|
102
103
|
upgrade: upgradeCommand,
|
|
103
104
|
check: checkCommand,
|
|
104
105
|
dev: devCommand,
|
|
106
|
+
edit: editCommand,
|
|
105
107
|
verify: verifyCommand,
|
|
106
108
|
build: buildCommand,
|
|
107
109
|
generate: generateCommand,
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,6FAA6F;AAC7F,2FAA2F;AAC3F,+FAA+F;AAC/F,gGAAgG;AAChG,8FAA8F;AAC9F,4EAA4E;AAE5E,gGAAgG;AAChG,+FAA+F;AAC/F,iFAAiF;AACjF,MAAM,WAAW,GAAG,IAAI,OAAO,EAAU,CAAC;AAE1C;;;;;GAKG;AACH,SAAS,aAAa,CACpB,OAAsB;IAEtB,MAAM,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC;IACjC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACxD,OAAO,IAA2C,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,eAAe,CAAoB,OAAsB;IACvE,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;IAChC,MAAM,OAAO,GAAkB,EAAE,GAAG,OAAO,EAAE,CAAC;IAE9C,IAAI,WAAW,EAAE,CAAC;QAChB,OAAO,CAAC,GAAG,GAAG,KAAK,EAAE,OAAO,EAAE,EAAE;YAC9B,IAAI,CAAC;gBACH,OAAO,MAAM,WAAW,CAAC,OAAO,CAAC,CAAC;YACpC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;oBAC9B,kFAAkF;oBAClF,uFAAuF;oBACvF,uFAAuF;oBACvF,yDAAyD;oBACzD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;oBAC7B,IAAI,OAAO,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,EAAE,CAAC;wBAChC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;oBAC9E,CAAC;oBACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBAC/B,CAAC;gBACD,uFAAuF;gBACvF,2EAA2E;gBAC3E,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC,CAAC;QACF,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,8FAA8F;IAC9F,+FAA+F;IAC/F,+FAA+F;IAC/F,8FAA8F;IAC9F,yFAAyF;IACzF,aAAa;IACb,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,IAAI,EAAE,CAAC;QACT,OAAO,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CACtC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CACxE,CAAC;IACJ,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAA4B;IACnE,IAAI,OAAO,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtC,CAAC;IACD,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACpC,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;AACvE,CAAC;AAED,4FAA4F;AAC5F,+FAA+F;AAC/F,+DAA+D;AAC/D,MAAM,WAAW,GAAG;IAClB,KAAK,EAAE,YAAY;IACnB,MAAM,EAAE,aAAa;IACrB,MAAM,EAAE,aAAa;IACrB,IAAI,EAAE,WAAW;IACjB,OAAO,EAAE,cAAc;IACvB,KAAK,EAAE,YAAY;IACnB,GAAG,EAAE,UAAU;IACf,MAAM,EAAE,aAAa;IACrB,KAAK,EAAE,YAAY;IACnB,QAAQ,EAAE,eAAe;IACzB,KAAK,EAAE,YAAY;IACnB,KAAK,EAAE,YAAY;IACnB,OAAO,EAAE,cAAc;CACxB,CAAC;AAIF,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAC1C,MAAM,CAAC,OAAO,CAAC,WAAW,CAA+C,CAAC,GAAG,CAC5E,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC,CACtD,CAC0C,CAAC;AAE9C,iGAAiG;AACjG,8FAA8F;AAC9F,uFAAuF;AACvF,4FAA4F;AAC5F,MAAM,CAAC,MAAM,IAAI,GAAG,aAAa,CAAC;IAChC,IAAI,EAAE;QACJ,IAAI,EAAE,UAAU;QAChB,WAAW,EAAE,gDAAgD;KAC9D;IACD,WAAW;CACZ,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const editCommand: import("citty").CommandDef<{
|
|
2
|
+
readonly port: {
|
|
3
|
+
readonly type: "string";
|
|
4
|
+
readonly description: "Port to serve the game on (default 3010).";
|
|
5
|
+
};
|
|
6
|
+
readonly 'editor-port': {
|
|
7
|
+
readonly type: "string";
|
|
8
|
+
readonly description: "Port to serve the editor on (default 3011).";
|
|
9
|
+
};
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bitmagic edit` — `dev` plus a visual scene editor.
|
|
3
|
+
*
|
|
4
|
+
* Everything `dev` does (build, `tsc --watch`, vite) plus a loopback sidecar that serves the editor
|
|
5
|
+
* shell and writes scene edits into `src/work/world.json`. Deliberately a superset rather than a
|
|
6
|
+
* flag on `dev`: `dev` is the plain run-the-game loop and must stay offline, minimal and boring,
|
|
7
|
+
* while `edit` opens a second port and a write path into the creator's project.
|
|
8
|
+
*/
|
|
9
|
+
import { defineCommand } from 'citty';
|
|
10
|
+
import { spawn, spawnSync } from 'child_process';
|
|
11
|
+
import { CliError } from '../errors.js';
|
|
12
|
+
import { loadProjectContext, projectBin } from '../project/context.js';
|
|
13
|
+
import { startEditorServer } from '../editor/server.js';
|
|
14
|
+
import { CORS_ALLOWED_PORT_MAX, CORS_ALLOWED_PORT_MIN } from '../local-port.js';
|
|
15
|
+
import { waitForServer } from '../verify/wait-for-server.js';
|
|
16
|
+
const DEFAULT_GAME_PORT = 3010;
|
|
17
|
+
const DEFAULT_EDITOR_PORT = 3011;
|
|
18
|
+
/**
|
|
19
|
+
* Both ports must sit inside the CDN's CORS window.
|
|
20
|
+
*
|
|
21
|
+
* The game port for the reason `local-port.ts` documents at length: a project fetches its forged
|
|
22
|
+
* level and its GLBs cross-origin from the portal CDN, which allowlists `localhost:3000`-`3199`
|
|
23
|
+
* and nothing else. Outside it the fetch is blocked, the world loads with no terrain, and the
|
|
24
|
+
* symptom reads as a broken game rather than a blocked request.
|
|
25
|
+
*
|
|
26
|
+
* The editor port is checked too, even though the shell itself fetches nothing from the CDN,
|
|
27
|
+
* because the two are adjacent by default and a creator who moves one will move the other — being
|
|
28
|
+
* told the rule once, at the point of the mistake, beats discovering it as a terrain-less world.
|
|
29
|
+
*/
|
|
30
|
+
function parsePort(value, fallback, flag) {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return fallback;
|
|
33
|
+
const port = Number(value);
|
|
34
|
+
if (!Number.isInteger(port)) {
|
|
35
|
+
throw new CliError(`\`${flag}\` must be a whole number, got "${value}".`);
|
|
36
|
+
}
|
|
37
|
+
if (port < CORS_ALLOWED_PORT_MIN || port > CORS_ALLOWED_PORT_MAX) {
|
|
38
|
+
throw new CliError(`\`${flag}\` must be between ${CORS_ALLOWED_PORT_MIN} and ${CORS_ALLOWED_PORT_MAX}. `
|
|
39
|
+
+ 'The asset CDN only allows those origins; outside that window the game loads with no terrain.');
|
|
40
|
+
}
|
|
41
|
+
return port;
|
|
42
|
+
}
|
|
43
|
+
export const editCommand = defineCommand({
|
|
44
|
+
meta: {
|
|
45
|
+
name: 'edit',
|
|
46
|
+
description: 'Serve the game with a visual scene editor, saving edits to world.json.',
|
|
47
|
+
},
|
|
48
|
+
args: {
|
|
49
|
+
port: { type: 'string', description: `Port to serve the game on (default ${DEFAULT_GAME_PORT}).` },
|
|
50
|
+
'editor-port': { type: 'string', description: `Port to serve the editor on (default ${DEFAULT_EDITOR_PORT}).` },
|
|
51
|
+
},
|
|
52
|
+
async run({ args }) {
|
|
53
|
+
const context = loadProjectContext();
|
|
54
|
+
const tsc = projectBin(context.root, 'tsc');
|
|
55
|
+
const vite = projectBin(context.root, 'vite');
|
|
56
|
+
const gamePort = parsePort(args.port, DEFAULT_GAME_PORT, '--port');
|
|
57
|
+
const editorPort = parsePort(args['editor-port'], DEFAULT_EDITOR_PORT, '--editor-port');
|
|
58
|
+
if (gamePort === editorPort) {
|
|
59
|
+
throw new CliError('`--port` and `--editor-port` must differ — the game and the editor are two servers.');
|
|
60
|
+
}
|
|
61
|
+
// Build to completion first, exactly as `dev` does: every alias resolves into dist/, so serving
|
|
62
|
+
// before the first emit 404s every module.
|
|
63
|
+
console.log('Building…');
|
|
64
|
+
const build = spawnSync(tsc, [], { cwd: context.root, stdio: 'inherit' });
|
|
65
|
+
if (build.error) {
|
|
66
|
+
throw new CliError(`Could not run the TypeScript compiler: ${build.error.message}`);
|
|
67
|
+
}
|
|
68
|
+
if (build.status !== 0) {
|
|
69
|
+
throw new CliError('Build failed. Fix the errors above, then run `bitmagic edit` again.', build.status ?? 1);
|
|
70
|
+
}
|
|
71
|
+
// Before the children, so a port clash fails fast rather than after vite has claimed its port.
|
|
72
|
+
const editor = await startEditorServer({
|
|
73
|
+
root: context.root,
|
|
74
|
+
gameId: context.metadata.gameId,
|
|
75
|
+
gamePort,
|
|
76
|
+
port: editorPort,
|
|
77
|
+
log: (message) => console.log(message),
|
|
78
|
+
});
|
|
79
|
+
const children = [
|
|
80
|
+
{
|
|
81
|
+
name: 'tsc --watch',
|
|
82
|
+
process: spawn(tsc, ['--watch', '--preserveWatchOutput'], { cwd: context.root, stdio: 'inherit' }),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'vite',
|
|
86
|
+
process: spawn(vite, ['--port', String(gamePort), '--strictPort'], { cwd: context.root, stdio: 'inherit' }),
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
let shuttingDown = false;
|
|
90
|
+
let requestedByUser = false;
|
|
91
|
+
const shutdown = () => {
|
|
92
|
+
if (shuttingDown)
|
|
93
|
+
return;
|
|
94
|
+
shuttingDown = true;
|
|
95
|
+
for (const child of children)
|
|
96
|
+
child.process.kill('SIGTERM');
|
|
97
|
+
void editor.close();
|
|
98
|
+
};
|
|
99
|
+
process.on('SIGINT', () => {
|
|
100
|
+
requestedByUser = true;
|
|
101
|
+
shutdown();
|
|
102
|
+
});
|
|
103
|
+
process.on('SIGTERM', () => {
|
|
104
|
+
requestedByUser = true;
|
|
105
|
+
shutdown();
|
|
106
|
+
});
|
|
107
|
+
void waitForServer(gamePort, 30_000)
|
|
108
|
+
.then(() => {
|
|
109
|
+
console.log(`\nServing the game at ${`http://localhost:${gamePort}/`}`);
|
|
110
|
+
console.log(`Editor ready at ${editor.url}`);
|
|
111
|
+
console.log('\nOpen the editor URL, click an object and drag the gizmo.');
|
|
112
|
+
console.log('Every change is saved to src/work/world.json.');
|
|
113
|
+
})
|
|
114
|
+
.catch(() => {
|
|
115
|
+
// The child-exit watcher below reports a vite that never comes up.
|
|
116
|
+
});
|
|
117
|
+
const failure = await new Promise((resolve) => {
|
|
118
|
+
let exitedCount = 0;
|
|
119
|
+
for (const child of children) {
|
|
120
|
+
child.process.on('error', (error) => {
|
|
121
|
+
shutdown();
|
|
122
|
+
resolve({ name: child.name, detail: `could not start: ${error.message}` });
|
|
123
|
+
});
|
|
124
|
+
child.process.on('exit', (code, signal) => {
|
|
125
|
+
exitedCount += 1;
|
|
126
|
+
if (!requestedByUser && !shuttingDown) {
|
|
127
|
+
shutdown();
|
|
128
|
+
resolve({ name: child.name, detail: `exited with code ${code}${signal ? ` (signal ${signal})` : ''}` });
|
|
129
|
+
}
|
|
130
|
+
else if (exitedCount === children.length) {
|
|
131
|
+
resolve(undefined);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
await editor.close();
|
|
137
|
+
if (failure) {
|
|
138
|
+
throw new CliError(`${failure.name} ${failure.detail}. \`bitmagic edit\` stopped.`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
//# sourceMappingURL=edit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit.js","sourceRoot":"","sources":["../../src/commands/edit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn the game engine's own scene-editing replies into a `WorldJsonModification[]`.
|
|
3
|
+
*
|
|
4
|
+
* This is the CLI lane's counterpart to the Creator's `useSave.ts saveSceneModifications` and the
|
|
5
|
+
* spawn-point/world-config half of `useTabSwitching.ts deactivateScene`. The web lane posts its
|
|
6
|
+
* modifications to `POST /api/edit-world-config`, which rebuilds `predicateField`/`predicateValue`
|
|
7
|
+
* into real predicates server-side because functions cannot cross HTTP. Here there is no server:
|
|
8
|
+
* the shell page posts the engine's replies VERBATIM to the local sidecar, and this module — which
|
|
9
|
+
* runs in Node, in the same process that writes the file — builds the predicates directly.
|
|
10
|
+
*
|
|
11
|
+
* That split is deliberate. Everything that knows what world.json means lives on this side, so it
|
|
12
|
+
* is plain, synchronous, dependency-free code that can be unit-tested without a browser; the shell
|
|
13
|
+
* stays a dumb relay of `SCENE_HAS_CHANGES` and `SCENE_EDITING_STATUS`.
|
|
14
|
+
*/
|
|
15
|
+
import type { WorldJsonModification, WorldJsonShape } from '@bitmagic/world-forger/pipeline/index.js';
|
|
16
|
+
/** The engine's `SCENE_HAS_CHANGES` reply (`CreatorMessageHandler.ts`, `CHECK_SCENE_CHANGES`). */
|
|
17
|
+
export interface SceneChangeReport {
|
|
18
|
+
hasChanges?: boolean;
|
|
19
|
+
modifiedObjectIds?: string[];
|
|
20
|
+
deletedObjectIds?: string[];
|
|
21
|
+
fullSaveNeeded?: boolean;
|
|
22
|
+
pendingWorldConfig?: {
|
|
23
|
+
settings: unknown;
|
|
24
|
+
path: string[];
|
|
25
|
+
title?: string;
|
|
26
|
+
} | null;
|
|
27
|
+
pendingSpawnPoints?: Array<Record<string, unknown>> | null;
|
|
28
|
+
}
|
|
29
|
+
/** The engine's `SCENE_EDITING_STATUS` reply (`CreatorMessageHandler.ts`, line ~1463). */
|
|
30
|
+
export interface SceneEditingStatus {
|
|
31
|
+
unlocked?: boolean;
|
|
32
|
+
environmentObjects?: Array<Record<string, unknown>>;
|
|
33
|
+
/**
|
|
34
|
+
* WHICH level `environmentObjects` covers. `serializeEnvironmentObjects()` walks the LIVE scene
|
|
35
|
+
* and a multi-level game only instantiates one level, so a non-null id means the array is this
|
|
36
|
+
* level plus untagged globals — never the whole world. `null` (single-level or legacy) means it
|
|
37
|
+
* genuinely is the whole world.
|
|
38
|
+
*/
|
|
39
|
+
activeLevelId?: string | null;
|
|
40
|
+
markers?: Array<Record<string, unknown>>;
|
|
41
|
+
}
|
|
42
|
+
/** What the shell POSTs to `/api/scene/save`. Just the two engine replies, plus one flag. */
|
|
43
|
+
export interface ScenePayload {
|
|
44
|
+
changes: SceneChangeReport;
|
|
45
|
+
status: SceneEditingStatus;
|
|
46
|
+
/** Set by the shell when the engine posted `ADD_MARKER` / `UPDATE_MARKER` since the last save. */
|
|
47
|
+
markersDirty?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the write for one scene-editing flush. Returns `[]` when there is nothing to persist, so
|
|
51
|
+
* the caller can skip the file write entirely.
|
|
52
|
+
*
|
|
53
|
+
* `world` is the CURRENT parsed world.json, needed only to resolve the levels registry. Everything
|
|
54
|
+
* else comes from the engine, which is the source of truth for scene state — this never invents a
|
|
55
|
+
* position, rotation or scale.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildSceneModifications(payload: ScenePayload, world: WorldJsonShape): WorldJsonModification[];
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EditorManager.markFullSaveNeeded()` pushes this sentinel into the same `Set` that holds real
|
|
3
|
+
* object ids, so it arrives in `modifiedObjectIds` and must be filtered out before any id is
|
|
4
|
+
* matched against an object. `fullSaveNeeded` carries the actual signal.
|
|
5
|
+
*/
|
|
6
|
+
const FULL_SAVE_SENTINEL = '__FULL_SAVE__';
|
|
7
|
+
/** Spawn markers are tracked in the same modified-id set but persist through `pendingSpawnPoints`. */
|
|
8
|
+
const SPAWN_ID_PREFIX = 'spawn_';
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
/** Match an array item by its `id`, the predicate every environmentObjects write uses. */
|
|
13
|
+
function byId(id) {
|
|
14
|
+
return (item) => isRecord(item) && item.id === id;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The spawn-point half, mirroring `useTabSwitching.ts deactivateScene` lines 234-303.
|
|
18
|
+
*
|
|
19
|
+
* The rule that matters: in a levels game the spawns belong to the ACTIVE level's entry, and the
|
|
20
|
+
* flat `spawnPoints` / `playerSpawn*` fields are only mirrored when that level is the start level.
|
|
21
|
+
* Published games carry frozen template code that reads only the flat fields, so the mirror cannot
|
|
22
|
+
* be dropped — but writing it from a non-start level would make the wrong level's spawn the one
|
|
23
|
+
* every published build uses.
|
|
24
|
+
*/
|
|
25
|
+
function spawnPointModifications(spawnPoints, activeLevelId, world) {
|
|
26
|
+
const player = spawnPoints.find((point) => point.type === 'player');
|
|
27
|
+
const globals = [
|
|
28
|
+
{ type: 'set', path: ['spawnPoints'], value: spawnPoints },
|
|
29
|
+
];
|
|
30
|
+
if (player?.position !== undefined) {
|
|
31
|
+
globals.push({ type: 'set', path: ['playerSpawnPosition'], value: player.position });
|
|
32
|
+
}
|
|
33
|
+
// Only when present: writing `undefined` would drop the key on stringify for a game that had a
|
|
34
|
+
// rotation before, silently resetting the player's facing.
|
|
35
|
+
if (player?.rotationY !== undefined) {
|
|
36
|
+
globals.push({ type: 'set', path: ['playerSpawnRotationY'], value: player.rotationY });
|
|
37
|
+
}
|
|
38
|
+
const levels = world.worldProfileData?.levels ?? [];
|
|
39
|
+
if (levels.length === 0)
|
|
40
|
+
return globals;
|
|
41
|
+
const firstLevel = levels[0];
|
|
42
|
+
if (firstLevel === undefined)
|
|
43
|
+
return globals;
|
|
44
|
+
const startLevelId = world.worldProfileData?.startLevelId ?? firstLevel.id;
|
|
45
|
+
const targetId = activeLevelId ?? startLevelId;
|
|
46
|
+
const level = levels.find((entry) => entry.id === targetId);
|
|
47
|
+
// No matching entry means the engine reported a level this world.json does not know about.
|
|
48
|
+
// Falling back to the globals keeps the edit rather than dropping it on the floor.
|
|
49
|
+
if (level === undefined)
|
|
50
|
+
return globals;
|
|
51
|
+
const levelModification = {
|
|
52
|
+
type: 'update',
|
|
53
|
+
path: ['levels'],
|
|
54
|
+
predicate: (item) => isRecord(item) && item.id === level.id,
|
|
55
|
+
// An updater rather than a `{ ...level, spawnPoints }` snapshot (which is what the Creator
|
|
56
|
+
// builds): the snapshot was read before the write and would silently revert any other field
|
|
57
|
+
// the agent changed on this level in between.
|
|
58
|
+
value: (item) => ({ ...(isRecord(item) ? item : {}), spawnPoints }),
|
|
59
|
+
};
|
|
60
|
+
return level.id === startLevelId ? [levelModification, ...globals] : [levelModification];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the write for one scene-editing flush. Returns `[]` when there is nothing to persist, so
|
|
64
|
+
* the caller can skip the file write entirely.
|
|
65
|
+
*
|
|
66
|
+
* `world` is the CURRENT parsed world.json, needed only to resolve the levels registry. Everything
|
|
67
|
+
* else comes from the engine, which is the source of truth for scene state — this never invents a
|
|
68
|
+
* position, rotation or scale.
|
|
69
|
+
*/
|
|
70
|
+
export function buildSceneModifications(payload, world) {
|
|
71
|
+
const { changes, status } = payload;
|
|
72
|
+
const environmentObjects = status.environmentObjects ?? [];
|
|
73
|
+
const activeLevelId = status.activeLevelId ?? null;
|
|
74
|
+
const deletedObjectIds = changes.deletedObjectIds ?? [];
|
|
75
|
+
const modifiedObjectIds = (changes.modifiedObjectIds ?? []).filter((id) => id !== FULL_SAVE_SENTINEL && !id.startsWith(SPAWN_ID_PREFIX));
|
|
76
|
+
const modifications = [];
|
|
77
|
+
// Deletions first: order is the contract for `applyModificationsToWorld`, and an id that is both
|
|
78
|
+
// deleted and re-added in the same flush must end up added.
|
|
79
|
+
for (const id of deletedObjectIds) {
|
|
80
|
+
modifications.push({
|
|
81
|
+
type: 'removeRoot',
|
|
82
|
+
path: ['environmentObjects'],
|
|
83
|
+
predicate: byId(id),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (changes.fullSaveNeeded === true) {
|
|
87
|
+
if (activeLevelId === null) {
|
|
88
|
+
// The array genuinely is the whole world, so replacing it wholesale is safe — and it is the
|
|
89
|
+
// only form that can DROP an object the engine no longer has but never reported deleting.
|
|
90
|
+
modifications.push({
|
|
91
|
+
type: 'setRoot',
|
|
92
|
+
path: ['environmentObjects'],
|
|
93
|
+
value: environmentObjects,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Deliberate divergence from the Creator, which issues a `setRoot` here. In a levels game the
|
|
98
|
+
// array holds only the active level plus globals, so a `setRoot` deletes every other level's
|
|
99
|
+
// scenery. Upserting each object writes exactly what the engine reported and touches nothing
|
|
100
|
+
// else.
|
|
101
|
+
for (const object of environmentObjects) {
|
|
102
|
+
const id = object.id;
|
|
103
|
+
if (typeof id !== 'string')
|
|
104
|
+
continue;
|
|
105
|
+
modifications.push({
|
|
106
|
+
type: 'upsertRoot',
|
|
107
|
+
path: ['environmentObjects'],
|
|
108
|
+
predicate: byId(id),
|
|
109
|
+
value: object,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const modifiedIds = new Set(modifiedObjectIds);
|
|
116
|
+
for (const object of environmentObjects) {
|
|
117
|
+
const id = object.id;
|
|
118
|
+
if (typeof id !== 'string' || !modifiedIds.has(id))
|
|
119
|
+
continue;
|
|
120
|
+
modifications.push({
|
|
121
|
+
type: 'upsertRoot',
|
|
122
|
+
path: ['environmentObjects'],
|
|
123
|
+
predicate: byId(id),
|
|
124
|
+
value: object,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (changes.pendingWorldConfig) {
|
|
129
|
+
const { path, settings } = changes.pendingWorldConfig;
|
|
130
|
+
if (Array.isArray(path) && path.length > 0) {
|
|
131
|
+
modifications.push({ type: 'set', path, value: settings });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (changes.pendingSpawnPoints && changes.pendingSpawnPoints.length > 0) {
|
|
135
|
+
modifications.push(...spawnPointModifications(changes.pendingSpawnPoints, activeLevelId, world));
|
|
136
|
+
}
|
|
137
|
+
if (payload.markersDirty === true) {
|
|
138
|
+
// Whole-array `set` rather than push/update: `SCENE_EDITING_STATUS` returns the live
|
|
139
|
+
// `markerSystem.serializeMarkers()` output, which already reflects adds, edits and removals.
|
|
140
|
+
modifications.push({ type: 'set', path: ['markers'], value: status.markers ?? [] });
|
|
141
|
+
}
|
|
142
|
+
return modifications;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=save.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save.js","sourceRoot":"","sources":["../../src/editor/save.ts"],"names":[],"mappings":"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"}
|
|
@@ -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"}
|
|
@@ -369,6 +369,18 @@ compile that dies on load — a missing asset, a bad \`world.json\` value, a nul
|
|
|
369
369
|
\`bitmagic verify\` after a change you cannot eyeball: it loads the game in a headless browser and
|
|
370
370
|
fails with what actually broke, leaving a console log and a screenshot in \`.bitmagic/verify/\`.
|
|
371
371
|
|
|
372
|
+
## The human may be moving objects too
|
|
373
|
+
|
|
374
|
+
\`bitmagic edit\` gives this project's owner a visual scene editor — they click an object and drag
|
|
375
|
+
a gizmo, and the new transform is written straight into \`src/work/world.json\`, one entry per
|
|
376
|
+
object touched. So \`world.json\` is shared ground, not yours alone:
|
|
377
|
+
|
|
378
|
+
- **Re-read \`src/work/world.json\` before editing it.** The copy you read earlier in the session
|
|
379
|
+
may be stale, and a whole-file rewrite from that copy silently reverts what they just placed.
|
|
380
|
+
- Prefer changing the entries you mean to change over regenerating \`environmentObjects\`
|
|
381
|
+
wholesale, so their placements survive your edit.
|
|
382
|
+
- \`git diff src/work/world.json\` shows what they moved; there is no separate edit history.
|
|
383
|
+
|
|
372
384
|
## Publishing
|
|
373
385
|
|
|
374
386
|
\`\`\`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-files.js","sourceRoot":"","sources":["../../src/scaffold/project-files.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"project-files.js","sourceRoot":"","sources":["../../src/scaffold/project-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEjF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAA2B;IAClD,2BAA2B,EAAE,SAAS;IACtC,2BAA2B,EAAE,SAAS;IACtC,kBAAkB,EAAE,QAAQ;IAC5B,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,kCAAkC,EAAE,QAAQ;IAC5C,KAAK,EAAE,SAAS;IAChB,KAAK,EAAE,UAAU;CAClB,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAA2B;IACtD,aAAa,EAAE,UAAU;IACzB,cAAc,EAAE,UAAU;IAC1B,8FAA8F;IAC9F,cAAc,EAAE,QAAQ;IACxB,4FAA4F;IAC5F,mFAAmF;IACnF,eAAe,EAAE,SAAS;IAC1B,UAAU,EAAE,QAAQ;IACpB,IAAI,EAAE,QAAQ;IACd,wBAAwB,EAAE,QAAQ;CACnC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,aAAa,GAAG,SAAS,CAAC;AAEhC;;;;;;;;;GASG;AACH,MAAM,UAAU,GAAG;IACjB,uBAAuB;IACvB,oCAAoC,aAAa,WAAW;IAC5D,kDAAkD,aAAa,2CAA2C;IAC1G,8CAA8C,aAAa,uCAAuC;IAClG,gDAAgD,aAAa,wCAAwC;CACtG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAEX,oGAAoG;AACpG,MAAM,WAAW,GAA2B;IAC1C,2FAA2F;IAC3F,4FAA4F;IAC5F,4FAA4F;IAC5F,KAAK,EAAE,UAAU;IACjB,cAAc,EAAE,wBAAwB,aAAa,SAAS;IAC9D,WAAW,EAAE,wBAAwB,aAAa,MAAM;IACxD,2FAA2F;IAC3F,6FAA6F;IAC7F,+FAA+F;IAC/F,4FAA4F;IAC5F,6FAA6F;IAC7F,6FAA6F;IAC7F,eAAe,EAAE,yBAAyB,aAAa,UAAU;IACjE,qBAAqB,EAAE,yBAAyB,aAAa,gBAAgB;IAC7E,QAAQ,EAAE,wBAAwB,aAAa,GAAG;IAClD,2BAA2B,EAAE,iDAAiD;IAC9E,2BAA2B,EAAE,iDAAiD;IAC9E,OAAO,EAAE,+BAA+B;IACxC,kCAAkC,EAAE,uDAAuD;IAC3F,kBAAkB,EAAE,uCAAuC;IAC3D,MAAM,EAAE,6BAA6B;CACtC,CAAC;AAEF,sFAAsF;AACtF,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,UAAU,CAAC;QAChB,IAAI;QACJ,OAAO,EAAE,OAAO;QAChB,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE;YACP,KAAK,EAAE,KAAK;YACZ,KAAK,EAAE,cAAc;YACrB,GAAG,EAAE,MAAM;SACZ;QACD,YAAY,EAAE,YAAY;QAC1B,eAAe,EAAE,gBAAgB;KAClC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,UAAU,CAAC;QAChB,eAAe,EAAE;YACf,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,QAAQ;YAChB,GAAG,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC;YACtB,OAAO,EAAE,GAAG;YACZ,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,GAAG;YACZ,KAAK,EAAE,aAAa,EAAE;YACtB,gBAAgB,EAAE,SAAS;YAC3B,MAAM,EAAE,IAAI;YACZ,wBAAwB,EAAE,IAAI;YAC9B,0BAA0B,EAAE,KAAK;YACjC,oBAAoB,EAAE,IAAI;YAC1B,eAAe,EAAE,IAAI;YACrB,eAAe,EAAE,OAAO;YACxB,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,IAAI;YACrB,4BAA4B,EAAE,IAAI;YAClC,gCAAgC,EAAE,IAAI;YACtC,aAAa,EAAE,IAAI;YACnB,iBAAiB,EAAE,IAAI;YACvB,SAAS,EAAE,IAAI;YACf,wFAAwF;YACxF,yFAAyF;YACzF,wFAAwF;YACxF,0FAA0F;YAC1F,sFAAsF;YACtF,yFAAyF;YACzF,gFAAgF;YAChF,KAAK,EAAE,CAAC,eAAe,CAAC;YACxB,SAAS,EAAE,CAAC,cAAc,EAAE,qBAAqB,EAAE,wCAAwC,CAAC;SAC7F;QACD,OAAO,EAAE,CAAC,UAAU,EAAE,aAAa,CAAC;QACpC,uFAAuF;QACvF,uFAAuF;QACvF,oFAAoF;QACpF,wFAAwF;QACxF,wDAAwD;QACxD,OAAO,EAAE;YACP,cAAc;YACd,MAAM;YACN,qBAAqB;YACrB,0BAA0B;YAC1B,kBAAkB;YAClB,uBAAuB;YACvB,oBAAoB;SACrB;KACF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,MAAM,YAAY,GAAG,gBAAgB,EAAE,CAAC;IACxC,MAAM,OAAO,GAAG,EAAE,GAAG,WAAW,EAAE,GAAG,YAAY,EAAE,CAAC;IACpD,6FAA6F;IAC7F,yFAAyF;IACzF,MAAM,gBAAgB,GAAG,GAAG,YAAY,CAAC,SAAS,CAAC,iBAAiB,CAAC;IACrE,0FAA0F;IAC1F,0FAA0F;IAC1F,wFAAwF;IACxF,2FAA2F;IAC3F,mDAAmD;IACnD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2BP,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;;;iCAG5B,gBAAgB;;;CAGhD,CAAC;AACF,CAAC;AAED;;;;;GAKG;AACH,SAAS,kBAAkB;IACzB,OAAO,MAAM,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,UAAU,GAAG,OAAO,MAAM,GAAG,CAAC;SACrD,IAAI,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;IACrC,OAAO;;;;;;;;;;;EAWP,OAAO;;;;;;;CAOR,CAAC;AACF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,uBAAuB;IACrC,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;IACrC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4BP,OAAO;;;;;;;;;;;;;;CAcR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjF,CAAC;AAiBD,MAAM,UAAU,kBAAkB,CAAC,QAAyB;IAC1D,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0NR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0DR,CAAC;AACF,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAa;IAC9C,MAAM,KAAK,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC;IAC3B,OAAO;;EAEP,KAAK;QACH,CAAC,CAAC,8GAA8G;QAChH,CAAC,CAAC,0KAA0K;;;;EAI9K,KAAK,IAAI,iEAAiE;;;;;;;;;;;;;;CAc3E,CAAC;AACF,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO;;;;;;;;;;;;;;CAcR,CAAC;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitmagic/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Build Bitmagic games from your own agent tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"citty": "^0.2.2",
|
|
20
20
|
"playwright-core": "^1.59.1",
|
|
21
21
|
"tar": "^7.4.3",
|
|
22
|
-
"@bitmagic/
|
|
23
|
-
"@bitmagic/
|
|
22
|
+
"@bitmagic/world-forger": "0.1.3",
|
|
23
|
+
"@bitmagic/asset-core": "0.1.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@eslint/js": "^9.38.0",
|