Game manifest format
VGAI Reference 0.3
Planned versioned generated-schema truth covering Versioned game manifest and world declarations.
planned · generated · immutable Reference 0.3
Lifecycle and constraints
Detailed constraints will publish with the generated or reviewed entry.
Runnable Learn material
Generated format inventory
Inventory only — AC-REF-002 is not passed. Valid parser-verified examples, narrative guidance, complete cross-field/runtime constraints, and version history remain deferred. This page must not be treated as a complete format contract.
Source: packages/engine/schemas/vgai-game.schema.json ·
80 branch-sensitive field records ·
69 source-described.
This source intentionally supersedes the frozen study boundary.
game-manifest · file · 80 fields
| Field / branch | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
# $ | object | yes | none | {"additionalProperties":false} | Game manifest (vgai.game.json) — the complete v2 project configuration |
# $.$schema | string | no | none | none | Optional JSON Schema URI for editor autocomplete and validation |
# $.manifestVersion | const 2 | yes | none | {"const":2} | Manifest file format version (v2 clean roots) |
# $.name | string | yes | none | none | Game display name |
# $.appId | string | no | none | none | Stable reverse-domain application id used by packaged targets |
# $.editorPort | integer | no | none | {"minimum":1024,"maximum":65535} | Stable preferred local editor port; launchers may fall forward when occupied |
# $.version | string | yes | none | none | The game's own version (used for orphan detection) |
# $.engine | object | yes | none | none | Engine version pin |
# $.engine.version | string | yes | none | none | Exact @vgai/engine semver this game was scaffolded/last upgraded against — an identity pin, not a range |
# $.roots | array | yes | none | {"minItems":1} | The Game's explicit, non-empty adapter-root composition. There is no implicit root. |
# $.roots[] | object | yes | none | {"additionalProperties":false} | Missing source description |
# $.roots[].id | string | yes | none | none | Unique id for this root within the manifest (enforced-unique, D-V6 — GameManifestSchema.roots rejects duplicates) |
# $.roots[].description | string | no | none | none | Optional one-line description of this root's game/content (e.g. an ingested game's blurb — the manifest-native replacement for a registry entry's description field) |
# $.roots[].adapter | anyOf (3) | yes | none | none | This root's adapter and sole rendering/lifecycle discriminator |
# $.roots[].adapteranyOf[0] | enum (3) | yes | none | {"enum":["threejs","pixijs","react"]} | Missing source description |
# $.roots[].adapteranyOf[1] | object | yes | none | {"additionalProperties":false} | Missing source description |
# $.roots[].adapter.moduleanyOf[1] | string | yes | none | none | Game-folder-relative path to a custom adapter module (trust boundary) |
# $.roots[].adapter.surfaceanyOf[1] | enum (3) | yes | none | {"enum":["threejs","pixijs","react"]} | Native surface exposed by this custom adapter contract |
# $.roots[].adapteranyOf[2] | object | yes | none | {"additionalProperties":false} | Missing source description |
# $.roots[].adapter.surfaceanyOf[2] | enum (3) | yes | none | {"enum":["threejs","pixijs","react"]} | Native surface captured from the unmodified game |
# $.roots[].adapter.ingestanyOf[2] | object | yes | none | none | Ingest adapter configuration for an unmodified game (a repo-vendored game or your own external folder) |
# $.roots[].adapter.ingest.strategyanyOf[2] | enum (4) | yes | none | {"enum":["shared","deduped","iframe-reachable","opaque-embed"]} | Capture mechanics rung this ingested root uses (§4) — also the root`s natural local capability tier |
# $.roots[].adapter.ingest.entryHtmlanyOf[2] | string | no | none | none | Entry HTML path, project-relative. WIRED for opaque-embed (D-W2, docs/WAVE4-FTUE-HARDENING-DESIGN.md): the file`s bytes are read verbatim through the project-static route and hosted in a sandboxed iframe (embed-only, no scene introspection), for both pixijs and threejs roots. Legal but NOT wired to a mount for iframe-reachable (a named error at resolve names this — use `bundleUrl` instead, the wired reachable route). |
# $.roots[].adapter.ingest.assetsanyOf[2] | object | no | none | {"additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}} | Path-substring -> served-URL rewrites for this ingested game (IngestGame.assets today) |
# $.roots[].adapter.ingest.assets{key}anyOf[2] | string | no | none | none | Missing source description |
# $.roots[].adapter.ingest.domStubsanyOf[2] | array | no | none | none | DOM API stub ids this ingested game requires to run headlessly/in-realm |
# $.roots[].adapter.ingest.domStubs[]anyOf[2] | string | yes | none | none | Missing source description |
# $.roots[].adapter.ingest.captureTimeoutMsanyOf[2] | number | no | none | none | Max time to wait for in-realm/cross-realm capture before failing loudly |
# $.roots[].adapter.ingest.bundleUrlanyOf[2] | string | no | none | none | Absolute URL of the externalized built game bundle — the WIRED iframe-reachable mount route (iframe-reachable only), for BOTH pixijs (Track P, ingest-iframe-2d.ts's mountIngestGame2DIframeReachableMulti) and threejs (D-W1, docs/WAVE4-FTUE-HARDENING-DESIGN.md, ingest-iframe-reachable-adapter.ts's mountIngestGameIframeReachableBundle). PRECONDITION: the bundle must externalize its root`s own runtime as a bare, un-rewritten import (`pixi.js` for pixijs, `three` for threejs — `vgai bundle` does this for you) so it resolves through the iframe importmap to the HOST instance the capture trap is installed on; a bundle with an inlined runtime is unreachable and degrades to the embed-only floor. Exactly one of bundleUrl/entryHtml is required when strategy is iframe-reachable. |
# $.roots[].adapter.ingest.baseHrefanyOf[2] | string | no | none | none | Iframe <base href> so the game's relative asset URLs resolve (iframe-reachable only). |
# $.roots[].adapter.ingest.assetBaseUrlanyOf[2] | string | no | none | none | Host-side basePath forced onto the trapped runtime's own asset resolution — pixi's `Assets.init` for a pixijs root, three's `DefaultLoadingManager.setURLModifier` for a threejs root (D-W1) — absolute, or root-relative (resolved against the editor page's own origin at mount time, ingest-mode.ts, so a static manifest never has to know the dev-server port) (iframe-reachable only — a document.write iframe keeps the parent window.location). |
# $.roots[].adapter.ingest.extraDepsanyOf[2] | array | no | none | none | Bare specifiers of this root's externalized deps beyond its own runtime (pixi.js/three — e.g. '@pixi/sound', 'gsap'). Resolved by the editor's host-namespace registry into module namespaces when known (D-P1 — the manifest carries data, not code); an unregistered specifier falls back to the open project's own node_modules (D-Z3, docs/WAVE3-ADAPTER-PLUMBING-DESIGN.md), served as a verbatim iframe importmap URL. Throws a named error if neither resolves it (iframe-reachable only). |
# $.roots[].adapter.ingest.extraDeps[]anyOf[2] | string | yes | none | none | Missing source description |
# $.roots[].adapter.ingest.pixiModuleUrlanyOf[2] | string | no | none | none | PIXI-ONLY: standalone matching-major pixi ESM URL to trap instead of the host pixi, for version-skewed games (e.g. a v6 game on a v8 host); this field's PRESENCE is how the skew is expressed (D-P2 — no separate strategy/tier for version skew) (iframe-reachable only). Declaring this on a `kind: "threejs"` root is schema-legal (this field is kind-agnostic here) but throws a NAMED not-supported error at resolve — three has no version-skew mount this wave (D-W7, docs/WAVE4-FTUE-HARDENING-DESIGN.md: a hypothetical `threeModuleUrl` is explicitly parked, no consumer exists). |
# $.roots[].adapter.ingest.bodyHtmlanyOf[2] | string | no | none | none | HTML injected into the iframe body before the game script boots (e.g. a `<div id="game-root">` the game expects to find) (iframe-reachable only). |
# $.roots[].scene | string | no | none | none | .vscn.json / .scn2d.json scene path (default adapters only — ingest adapters keep their own formats) |
# $.roots[].entry | string | no | none | none | Module exporting setup()/adapter (default adapters' code-authored alternative to `scene`) |
# $.roots[].zOrder | integer | no | 0 | {"minimum":-9007199254740991,"maximum":9007199254740991} | Canvas stacking order (COMPOSITION-DESIGN); ties broken by array order |
# $.roots[].pausable | boolean | no | true | none | Whether play-mode pause/step applies to this root |
# $.roots[].loop | enum (2) | no | "gated" | {"enum":["gated","self-driven"]} | gated: host-driven loop (default). self-driven: this root drives its own loop (the "composited, unsynchronized" tier) — an axis independent of capability tier |
# $.roots[].capabilities | object | no | none | none | Per-delivery-context capability tier declarations (§4). Declaring above the adapter's ceiling for that context is an error; declaring at or below it (including `unsupported`) is legal. |
# $.roots[].capabilities.local | enum (6) | no | none | {"enum":["first-party","shared","deduped","iframe-reachable","opaque-embed","unsupported"]} | Declared capability tier when served via the local CLI (Vite pipeline); omitted -> derived (§4) |
# $.roots[].capabilities.hosted | enum (6) | no | none | {"enum":["first-party","shared","deduped","iframe-reachable","opaque-embed","unsupported"]} | Declared capability tier when served hosted (no bundler: esbuild-wasm + externals only); omitted -> derived (§4) |
# $.authoring | object | no | none | none | Editor-only authoring metadata; never runtime ownership |
# $.authoring.hierarchy | object | no | none | none | Editor projection for native adapter roots |
# $.authoring.hierarchy.rootLabels | object | no | none | {"additionalProperties":{"type":"string"},"propertyNames":{"type":"string"}} | Optional editor labels keyed by adapter-root id |
# $.authoring.hierarchy.rootLabels{key} | string | no | none | none | Missing source description |
# $.authoring.hierarchy.groups | array | no | none | none | Optional editor-only hierarchy groups |
# $.authoring.hierarchy.groups[] | object | yes | none | none | Missing source description |
# $.authoring.hierarchy.groups[].id | string | yes | none | none | Unique editor-only group id |
# $.authoring.hierarchy.groups[].label | string | yes | none | none | Editor-only group label |
# $.authoring.hierarchy.groups[].roots | array | yes | none | {"minItems":1} | Adapter-root ids shown under this editor-only group |
# $.authoring.hierarchy.groups[].roots[] | string | yes | none | none | Missing source description |
# $.server | object | no | none | none | Multiplayer server config (Colyseus), if this game registers a room |
# $.server.room | string | yes | none | none | Colyseus room name |
# $.server.module | string | no | none | none | Module path for the Colyseus room bootstrap; defaults to the scaffolded server bootstrap |
# $.resolution | object | no | none | none | Canvas resolution |
# $.resolution.width | number | yes | none | none | Canvas width in pixels |
# $.resolution.height | number | yes | none | none | Canvas height in pixels |
# $.debug | object | no | none | none | Debug-bridge production gating (D18) — governs whether ?vgai-debug=1 installs window.__vgai outside dev builds. |
# $.debug.allowInProduction | boolean | yes | none | none | Allow the ?vgai-debug=1 introspection bridge and debug-command invocation in production builds. Default false: the bridge only installs in dev builds. The runtime reader is the debug-bridge installer (D18). |
# $.determinism | object | no | none | none | Seeded-RNG determinism contract (D15, docs/D15-DETERMINISM-DESIGN.md) — governs whether ctx.random is boot-seeded from a reproducible seed and whether the gameplay-rng-ban scan / dev-mode Math.random phase trap are active for this project. |
# $.determinism.seededRandom | boolean | yes | none | none | Declares that ALL gameplay RNG in this project flows through ctx.random (the named-stream seeded PRNG, docs/D15-DETERMINISM-DESIGN.md) rather than raw Math.random/Date.now/performance.now. Three runtime enforcers key off this flag: the boot-time seeding reader (mount-manifest.ts seeds ctx.random from defaultSeed/?vgai-seed=/explicit config, in that precedence, only when true), the gameplay-rng-ban burn-down scan (test/gameplay-rng-ban.test.ts — lints this project's src/ for raw RNG/wall-clock calls), and the dev-mode Math.random phase trap (runtime/gameplay-rng-trap.ts — warns once per call site during a gameplay frame, never throws). Default false: an undeclared project gets no determinism contract at all (that IS the opt-out — no dead field, per the T4.1 policy). |
# $.determinism.defaultSeed | integer | no | none | {"minimum":-9007199254740991,"maximum":9007199254740991} | The seed ctx.random boots from when seededRandom is true, unless overridden by ?vgai-seed=<int> (the query param always wins over this manifest default) or an even higher-precedence explicit config value a host passes directly. Optional — omit to fall back to the runtime's own fixed default seed. |
# $.budget | object | no | none | none | Performance budget (W3d, F11 perf regression gates) asserted by `vgai perf --assert-budget`: a seeded, fixed-step headless run samples the engine profiler and fails (nonzero exit) when any metric here is exceeded. Only headlessly-measurable metrics are budgetable; real GPU frame time is not (SwiftShader). |
# $.budget.frameCpuMsP95 | number | no | none | {"exclusiveMinimum":0} | Maximum allowed p95 of per-frame CPU time (ms) across the measured frames of a `vgai perf` seeded headless run. CPU-side only: headless runs rasterize under SwiftShader, so this is main-thread frame cost, NOT real GPU frame time. Machine-dependent — prefer the baseline-diff tolerance band for regression gating and use this as a coarse absolute ceiling. |
# $.budget.physicsMsP95 | number | no | none | {"exclusiveMinimum":0} | Maximum allowed p95 of the profiler's `physics` phase time (ms) per fixed-step frame in a `vgai perf` run. Same CPU-timing caveats as frameCpuMsP95. |
# $.budget.maxEntities | integer | no | none | {"minimum":0,"maximum":9007199254740991} | Maximum allowed entity-tagged scene-graph nodes (nodes carrying userData.entityId) summed across the game's threejs/pixijs worlds at the end of a `vgai perf` run. Exact and deterministic under a seed — no tolerance band. |
# $.budget.maxDrawCalls | integer | no | none | {"maximum":9007199254740991,"exclusiveMinimum":0} | Maximum allowed renderer draw calls in any measured frame of a `vgai perf` run (the profiler's per-frame render.drawCalls, from renderer.info). Exact and deterministic under a seed. |
# $.budget.maxTriangles | integer | no | none | {"maximum":9007199254740991,"exclusiveMinimum":0} | Maximum allowed rendered triangles in any measured frame of a `vgai perf` run (the profiler's per-frame render.triangles, from renderer.info). Exact and deterministic under a seed. |
# $.learn | object | no | none | {"additionalProperties":false} | Optional learner-facing metadata (FT-5) — set on shipped example projects so the gallery/wizard/Learn-catalog registries can be generated from the manifest itself |
# $.learn.kind | enum (4) | yes | none | {"enum":["starter","feature","sample-game","lesson-companion"]} | Content kind in the FTUE taxonomy (§3): starter (minimal per-genre starting point), feature (demonstrates a subsystem), sample-game (complete game as an architectural reference), lesson-companion (exists to back a Learn-site lesson) |
# $.learn.title | string | yes | none | {"minLength":1} | Learner-facing display title (gallery card headline) |
# $.learn.summary | string | yes | none | {"minLength":1} | One-sentence learner-facing summary (gallery card body) |
# $.learn.difficulty | enum (3) | yes | none | {"enum":["beginner","intermediate","advanced"]} | Learner-facing difficulty rating |
# $.learn.features | array | yes | none | none | Short kebab-case feature tags used as gallery/catalog filter facets |
# $.learn.features[] | string | yes | none | {"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$"} | Missing source description |
# $.learn.thumbnail | string | no | none | none | Optional thumbnail image reference (engine-repo-relative, e.g. docs/example-screenshots/<id>/<shot>.png; generated per FT-11, never hand-made) |
# $.learn.preview | string | no | none | none | Optional short preview video/loop reference (generated per FT-11) |
# $.learn.lesson | string | no | none | none | Optional Learn-site lesson URL that teaches this project 1:1 (e.g. https://vgai-learn.pages.dev/quick-starts/joints/) |