@vgai/engine 0.5.3 → 0.5.5
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/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/engine",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.5",
|
|
6
6
|
"description": "Readable TypeScript game engine and universal host for Three.js, PixiJS, and React games.",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"game-engine",
|
|
@@ -22,6 +22,7 @@ import type { ResolvedAdapterRoot } from '../manifest/load';
|
|
|
22
22
|
import type { GameSession } from './create-runtime';
|
|
23
23
|
import { type MountEntry, mountManifestRoots, resolveManifest } from './mount-manifest';
|
|
24
24
|
import type { GameSetupFn } from './types';
|
|
25
|
+
import { assertExportedOrInEditor } from './unexported-game-trap';
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// The host contract (design/26 §5 D1's `ManifestHost`)
|
|
@@ -232,6 +233,11 @@ export async function mountGameFromManifest(
|
|
|
232
233
|
host: ManifestHost,
|
|
233
234
|
opts: MountGameOptions = {},
|
|
234
235
|
): Promise<GameSession> {
|
|
236
|
+
// Owner invariant: an unexported game never mounts outside the editor. The
|
|
237
|
+
// editor mounts through its own resolver, so every caller of THIS function
|
|
238
|
+
// is a standalone page — and on a dev server, that page refuses to boot
|
|
239
|
+
// (see unexported-game-trap.ts for why this is a trap and not a rule).
|
|
240
|
+
assertExportedOrInEditor();
|
|
235
241
|
const manifest = resolveManifest(manifestInput);
|
|
236
242
|
const entries: Record<string, MountEntry> = { ...opts.entries };
|
|
237
243
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE UNEXPORTED-GAME TRAP: a dev-served standalone game page REFUSES to boot.
|
|
3
|
+
*
|
|
4
|
+
* Owner decision (2026-08-09): until a game is EXPORTED, it is never played
|
|
5
|
+
* outside the editor. The editor is the one authoring, viewing and
|
|
6
|
+
* verification surface; the standalone page exists to serve the exported
|
|
7
|
+
* artifact, not to be a second door during development.
|
|
8
|
+
*
|
|
9
|
+
* Why this is a trap in CODE and not a rule in prose: the rule already
|
|
10
|
+
* existed, and it drifted. When an editor session broke mid-task, an agent
|
|
11
|
+
* "temporarily" switched to `npm run game`, the fallback fed its whole
|
|
12
|
+
* feedback loop (pixels for screenshots), nothing broken ever pushed it back,
|
|
13
|
+
* and hours of verification ran outside the editor without anyone deciding
|
|
14
|
+
* that. A workaround whose cost lands outside the loop that chose it is
|
|
15
|
+
* sticky; the only fix that holds is for the workaround PATH ITSELF to fail
|
|
16
|
+
* immediately, loudly, and with the way back in its hands. That is this file.
|
|
17
|
+
*
|
|
18
|
+
* The boundary is DEV-SERVED vs EXPORTED, decided by the bundler: a Vite dev
|
|
19
|
+
* server strips nothing, so `import.meta.env.DEV` is true; a production build
|
|
20
|
+
* (`vite build` — what `vgai deploy` runs) compiles it false and this module
|
|
21
|
+
* costs an exported game nothing. Headless contexts (unit tests, playtest's
|
|
22
|
+
* node leg) have no real browser page and never trip it — the check requires
|
|
23
|
+
* a document whose `defaultView` is the window, which no test stub wires up.
|
|
24
|
+
*
|
|
25
|
+
* There is deliberately NO opt-out flag, env var, or query param. An escape
|
|
26
|
+
* hatch an agent can reach for is the workaround again, one hop later.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** What the refusal says — one place, so the page and the thrown Error agree. */
|
|
30
|
+
const REFUSAL = [
|
|
31
|
+
'UNEXPORTED GAME, OUTSIDE THE EDITOR — refusing to mount.',
|
|
32
|
+
'',
|
|
33
|
+
'This game has not been exported. Until it is, the ONLY way to run, see,',
|
|
34
|
+
'or drive it is the editor:',
|
|
35
|
+
'',
|
|
36
|
+
' vgai edit . open (or reuse) the editor for this project',
|
|
37
|
+
' vgai play enter play mode from the terminal',
|
|
38
|
+
" vgai eval '<js>' drive and read the running game",
|
|
39
|
+
'',
|
|
40
|
+
'The standalone page serves EXPORTED builds only (`vgai deploy`, or the',
|
|
41
|
+
'production build it runs). Do not script around this page — no headless',
|
|
42
|
+
'browsers, bots, or screenshots against the dev server. Every agent',
|
|
43
|
+
'workflow goes through the editor session.',
|
|
44
|
+
].join('\n');
|
|
45
|
+
|
|
46
|
+
/** True only on a real, dev-served browser page — the one context the trap is for. */
|
|
47
|
+
function isDevServedBrowserPage(): boolean {
|
|
48
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return false;
|
|
49
|
+
// Test stubs install a bare `document` object; only a real page has the
|
|
50
|
+
// document↔window linkage.
|
|
51
|
+
if (document.defaultView !== window) return false;
|
|
52
|
+
return Boolean(import.meta.env?.DEV);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Throws (and paints a full-screen refusal, so a human at the tab sees it as
|
|
57
|
+
* immediately as a harness watching the console does) when an unexported game
|
|
58
|
+
* is being mounted on a dev-served page. Called by `mountGameFromManifest`
|
|
59
|
+
* before any root is resolved; exported builds and headless tests pass
|
|
60
|
+
* through untouched.
|
|
61
|
+
*/
|
|
62
|
+
export function assertExportedOrInEditor(): void {
|
|
63
|
+
if (!isDevServedBrowserPage()) return;
|
|
64
|
+
|
|
65
|
+
const screen = document.createElement('pre');
|
|
66
|
+
screen.textContent = REFUSAL;
|
|
67
|
+
screen.style.cssText = [
|
|
68
|
+
'position:fixed',
|
|
69
|
+
'inset:0',
|
|
70
|
+
'z-index:2147483647',
|
|
71
|
+
'margin:0',
|
|
72
|
+
'padding:48px',
|
|
73
|
+
'background:#1A0E0E',
|
|
74
|
+
'color:#FF9B8A',
|
|
75
|
+
'font:600 15px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace',
|
|
76
|
+
'white-space:pre-wrap',
|
|
77
|
+
].join(';');
|
|
78
|
+
document.body.appendChild(screen);
|
|
79
|
+
document.title = 'UNEXPORTED — use the editor';
|
|
80
|
+
|
|
81
|
+
throw new Error(`mountGameFromManifest: ${REFUSAL}`);
|
|
82
|
+
}
|