Skip to content

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 / branchTypeRequiredDefaultConstraintsDescription
# $ 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/)