@volter/editor-game 0.5.66 → 0.5.68

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.
Files changed (239) hide show
  1. package/contributions/audio-unlock.service.ts +2 -2
  2. package/contributions/autoplay.service.ts +1 -1
  3. package/contributions/bridge.command.ts +3 -3
  4. package/contributions/build-player.document.tsx +95 -0
  5. package/contributions/canvas/component-board.service.ts +16 -0
  6. package/contributions/canvas/design-time-mount.service.ts +45 -0
  7. package/contributions/canvas-story-capture.service.ts +12 -0
  8. package/contributions/edit-mode-audio.service.ts +2 -2
  9. package/contributions/edit-mode-networking.service.ts +1 -1
  10. package/contributions/gameplay.command.ts +4 -4
  11. package/contributions/generation.service.ts +1 -1
  12. package/contributions/godot.style.ts +26 -6
  13. package/contributions/godot.view.ts +6 -0
  14. package/contributions/ingest.service.ts +2 -2
  15. package/contributions/instances.command.ts +1 -1
  16. package/contributions/navmesh.menu.ts +1 -1
  17. package/contributions/network-observer.service.ts +14 -0
  18. package/contributions/play.command.ts +10 -4
  19. package/contributions/react/component-board.service.ts +1 -1
  20. package/contributions/react/design-time-mount.service.ts +1 -1
  21. package/contributions/scene-document.service.ts +3 -3
  22. package/contributions/state-watch.menu.ts +1 -1
  23. package/contributions/state-watch.utility.tsx +1 -1
  24. package/contributions/team-playtest.service.ts +4 -4
  25. package/contributions/three/component-board.service.ts +1 -1
  26. package/contributions/three/component-verbs.command.ts +6 -6
  27. package/contributions/three/three-authoring.service.ts +8 -5
  28. package/contributions/three-story-capture.service.ts +12 -0
  29. package/contributions/unity.style.ts +17 -7
  30. package/contributions/unity.view.ts +6 -0
  31. package/contributions/unreal.style.ts +10 -2
  32. package/contributions/unreal.view.ts +5 -0
  33. package/package.json +21 -10
  34. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  35. package/src/asset-budget/asset-budget-model.ts +1 -1
  36. package/src/audio/AudioDebuggerPanel.tsx +1 -1
  37. package/src/bridge/dispatch.ts +14 -14
  38. package/src/bridge/live-frames.ts +1 -1
  39. package/src/bridge/screenshot.ts +3 -3
  40. package/src/build/BuildProfilesPanel.tsx +14 -3
  41. package/src/build/build-session.ts +35 -0
  42. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +749 -0
  43. package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
  44. package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
  45. package/src/canvas/canvas-design-mount.ts +524 -0
  46. package/src/canvas/design-time-canvas-mount.ts +79 -0
  47. package/src/coverage/live-authoring-surface.ts +5 -5
  48. package/src/coverage/live-project-verbs.ts +1 -1
  49. package/src/coverage/native-system-coverage.ts +5 -5
  50. package/src/coverage/root-coverage.ts +1 -1
  51. package/src/coverage/session-coverage.ts +3 -3
  52. package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
  53. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
  54. package/src/edit-mode/edit-mode-audio.ts +4 -4
  55. package/src/edit-mode/edit-mode-networking.ts +2 -2
  56. package/src/game-document/GameCaptureFrameButton.tsx +1 -1
  57. package/src/game-document/GameDocument.tsx +2 -2
  58. package/src/game-document/GamePanel.tsx +4 -4
  59. package/src/game-document/InstanceInspectorPicker.tsx +2 -2
  60. package/src/game-document/crowd-debug.ts +1 -1
  61. package/src/game-document/device-preview.ts +10 -11
  62. package/src/game-document/physics-debug.ts +1 -1
  63. package/src/generation/GenerationActivity.tsx +3 -3
  64. package/src/generation/generation-documents.tsx +5 -5
  65. package/src/generation/generation-jobs.ts +1 -1
  66. package/src/host/adapter-runtime-bindings.ts +96 -5
  67. package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
  68. package/src/host/authoring/gesture-persist.ts +1 -1
  69. package/src/host/authoring/ingest-data-writer.ts +1 -1
  70. package/src/host/authoring/ingest-source-persistence.ts +4 -4
  71. package/src/host/authoring/mounted-authoring.ts +4 -4
  72. package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
  73. package/src/host/authoring/pixi-authoring-adapter.ts +178 -31
  74. package/src/host/authoring/pixi-creatable-kinds.ts +62 -0
  75. package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
  76. package/src/host/authoring/pixi-live-write-target.ts +4 -4
  77. package/src/host/authoring/pixi-source-identity.ts +3 -3
  78. package/src/host/authoring/pixi-source-write-target.ts +1709 -0
  79. package/src/host/authoring/pixi-still-presentation.ts +1 -1
  80. package/src/host/authoring/pixi-structure-history.ts +2 -2
  81. package/src/host/authoring/pixi-transform-channels.ts +16 -14
  82. package/src/host/authoring/source-persistence-backend.ts +3 -3
  83. package/src/host/authoring/struct-write-pipe.ts +1 -1
  84. package/src/host/binding-resolver.ts +8 -9
  85. package/src/host/browser-transpile.ts +1 -1
  86. package/src/host/canvas-entry-runtime.ts +58 -47
  87. package/src/host/canvas-preview-frames.ts +482 -0
  88. package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
  89. package/src/host/components/HeaderTelemetry.tsx +4 -4
  90. package/src/host/components/PixiIsolationSceneContent.tsx +11 -11
  91. package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
  92. package/src/host/components/frame-debugger-model.ts +2 -2
  93. package/src/host/components/header-telemetry-model.ts +2 -2
  94. package/src/host/components/scene-document.tsx +14 -14
  95. package/src/host/components/utility-view-state.ts +1 -1
  96. package/src/host/components/world-root-stage-binding.tsx +12 -12
  97. package/src/host/components/world-root-stage.ts +70 -49
  98. package/src/host/coverage/system-adapter-coverage.ts +3 -4
  99. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
  100. package/src/host/document-preview-three.ts +1 -1
  101. package/src/host/entry-adjudication.ts +6 -6
  102. package/src/host/game-css-scope-transform.ts +4 -0
  103. package/src/host/game-realm-page.ts +1 -1
  104. package/src/host/gameplay-export.ts +25 -14
  105. package/src/host/gameplay-recording.ts +5 -5
  106. package/src/host/gated-globals.ts +2 -2
  107. package/src/host/history/json-history-resource.ts +1 -1
  108. package/src/host/projection/pixi.ts +24 -2
  109. package/src/host/r3f-entry-runtime.ts +65 -34
  110. package/src/host/react-mount-runtime.ts +7 -48
  111. package/src/host/realm-services.ts +1 -1
  112. package/src/host/roots/canvas-root.tsx +373 -0
  113. package/src/host/roots/r3f-root.tsx +473 -0
  114. package/src/host/roots/react-root.ts +9 -43
  115. package/src/host/served-bundle-runtime-modules.ts +3 -19
  116. package/src/host/server-log-bridge.ts +2 -2
  117. package/src/host/stories/mounted-story-viewport-source.ts +1 -1
  118. package/src/host/stories/pixi-story-model.ts +30 -0
  119. package/src/host/stories/story-media-captures.ts +46 -0
  120. package/src/host/stories/story-media-presence.ts +3 -3
  121. package/src/host/stories/story-pixi-preview.ts +408 -0
  122. package/src/host/stories/story-three-preview.ts +806 -0
  123. package/src/host/stories/three-story-captures.ts +35 -0
  124. package/src/host/story-three-preview-runtime.ts +56 -0
  125. package/src/host/use-active-performance-source.ts +2 -2
  126. package/src/host/viewport-pose-memory.ts +1 -1
  127. package/src/host/viewport-root-presentation.ts +6 -5
  128. package/src/ingest/active-ingest.ts +1 -1
  129. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  130. package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
  131. package/src/ingest/deferred-ingest-play.ts +6 -6
  132. package/src/ingest/discovery-public-ingest.ts +2 -2
  133. package/src/ingest/ingest-boot-viewport.ts +2 -2
  134. package/src/ingest/ingest-canvas-scene-document.tsx +9 -9
  135. package/src/ingest/ingest-canvas-scene.ts +3 -3
  136. package/src/ingest/ingest-evidence-hook.ts +1 -1
  137. package/src/ingest/ingest-frame-snapshot.ts +1 -1
  138. package/src/ingest/ingest-play-control.ts +1 -1
  139. package/src/ingest/ingest-render-debug.ts +10 -10
  140. package/src/ingest/ingest-siblings.ts +13 -25
  141. package/src/ingest/module-mode.ts +14 -14
  142. package/src/ingest/mount-canvas-ingest-root.ts +21 -21
  143. package/src/ingest/mount-coverage.ts +2 -2
  144. package/src/ingest/mount-dom-ingest-root.ts +11 -11
  145. package/src/ingest/mount-ingest-root.ts +8 -8
  146. package/src/ingest/mount-three-ingest-root.ts +8 -8
  147. package/src/ingest/resolve-canvas.ts +1 -1
  148. package/src/ingest/served-html-boot.ts +1 -1
  149. package/src/ingest/surface-canvas.ts +1 -1
  150. package/src/ingest/unmount-ingest-root.ts +5 -5
  151. package/src/navmesh/navmesh-handler.ts +24 -16
  152. package/src/network/NetworkInspectorPanel.tsx +939 -37
  153. package/src/network/network-inspector-model.ts +20 -2
  154. package/src/play/play-log-events.ts +1 -1
  155. package/src/play/play-mode.ts +76 -133
  156. package/src/play/play-recording.ts +1 -1
  157. package/src/play/react-play-live-authoring.ts +3 -3
  158. package/src/play/run-selection.ts +93 -0
  159. package/src/play-bar/PlayBar.tsx +20 -39
  160. package/src/profiler/FrameDebuggerPanel.tsx +1 -1
  161. package/src/profiler/PerformancePanel.tsx +3 -3
  162. package/src/react/design-time-react-mount.ts +23 -65
  163. package/src/react/dom-authoring-adapter.ts +9 -9
  164. package/src/react/react-inspector-section.tsx +5 -5
  165. package/src/react/react-world-authoring-adapter.ts +11 -11
  166. package/src/react/story-documents/story-documents.tsx +9 -9
  167. package/src/react/ui-board-document.tsx +7 -7
  168. package/src/react/ui-component-board.ts +2 -2
  169. package/src/runtime/adapter/audio-meter.ts +21 -0
  170. package/src/runtime/adapter/first-party-audio-system.ts +230 -0
  171. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
  172. package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
  173. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
  174. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
  175. package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
  176. package/src/runtime/adapter/native-debug-module.ts +498 -0
  177. package/src/runtime/audio/bus-mixer.ts +161 -0
  178. package/src/runtime/audio/pose-guard.ts +80 -0
  179. package/src/runtime/core/frame-pacing.ts +126 -0
  180. package/src/runtime/core/game-loop.ts +225 -0
  181. package/src/runtime/core/game-scoped-slot.ts +28 -0
  182. package/src/runtime/core/seeded-random.ts +162 -0
  183. package/src/runtime/core/sim-clock.ts +391 -0
  184. package/src/runtime/core/system-runner.ts +269 -0
  185. package/src/runtime/core/types.ts +104 -0
  186. package/src/runtime/create-runtime.ts +1128 -0
  187. package/src/runtime/debug-bridge.ts +570 -0
  188. package/src/runtime/debug-registry.ts +899 -0
  189. package/src/runtime/dev/chrome-trace.ts +153 -0
  190. package/src/runtime/dev/instruments.ts +403 -0
  191. package/src/runtime/dev/logger.ts +119 -0
  192. package/src/runtime/dev/performance-profiler.ts +367 -0
  193. package/src/runtime/dev/register-render-vitals.ts +276 -0
  194. package/src/runtime/dev/render-census.ts +354 -0
  195. package/src/runtime/dev/render-debug-adapter.ts +218 -0
  196. package/src/runtime/dev/render-memory.ts +226 -0
  197. package/src/runtime/dev/render-vitals.ts +338 -0
  198. package/src/runtime/dev/static-batch-advisor.ts +188 -0
  199. package/src/runtime/dev/webgl-frame-capture.ts +366 -0
  200. package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
  201. package/src/runtime/dev-build.ts +47 -0
  202. package/src/runtime/game.ts +1636 -0
  203. package/src/runtime/gameplay-rng-trap.ts +135 -0
  204. package/src/runtime/host-context.ts +64 -0
  205. package/src/runtime/input-router.ts +182 -0
  206. package/src/runtime/mount-manifest.ts +480 -0
  207. package/src/runtime/pixi/authoring.ts +706 -0
  208. package/src/runtime/pixi/ingest.ts +116 -0
  209. package/src/runtime/pixi/physics-registry.ts +49 -0
  210. package/src/runtime/pixi/render-pass-bracket.ts +117 -0
  211. package/src/runtime/pixi/scene-capture.ts +179 -0
  212. package/src/runtime/pixi/system-adapters.ts +69 -0
  213. package/src/runtime/playtest.ts +22 -0
  214. package/src/runtime/presentation.ts +141 -0
  215. package/src/runtime/render-control.ts +642 -0
  216. package/src/runtime/render-seed.ts +77 -0
  217. package/src/runtime/run-ticks-settled.ts +73 -0
  218. package/src/runtime/setup/setup-audio.ts +72 -0
  219. package/src/services/audio-pose-guard.ts +2 -2
  220. package/src/services/game-audio.ts +152 -0
  221. package/src/services/game-network.ts +767 -0
  222. package/src/services/game-physics.ts +334 -0
  223. package/src/state-watch/StateWatchPanel.tsx +1 -1
  224. package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
  225. package/src/three/authoring/constraint-inspector-section.tsx +6 -5
  226. package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
  227. package/src/three/authoring/oid-source-persistence.ts +7 -7
  228. package/src/three/authoring/r3f-design-session.ts +58 -54
  229. package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
  230. package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
  231. package/src/three/authoring/three-authoring-adapter.ts +29 -29
  232. package/src/three/component-verbs/extract-menu.ts +7 -6
  233. package/src/three/component-verbs/fork-menu.ts +7 -6
  234. package/src/three/component-verbs/internals-menu.ts +2 -2
  235. package/src/three/story-documents/three-story-documents.tsx +13 -13
  236. package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
  237. package/src/three/three-board/board-scene.ts +6 -6
  238. package/src/three/three-board/three-component-board.ts +2 -2
  239. package/src/services/game-audio-unlock.ts +0 -48
@@ -0,0 +1,480 @@
1
+ // The standalone manifest-mount helper: an ENGINE-side generalization of
2
+ // `examples/tri-world/src/main.ts`'s hand-built `RootMountSpec[]`.
3
+ //
4
+ // `resolveAllRoots`/`resolveRootBinding` (`packages/editor/src/
5
+ // binding-resolver.ts`) already do this translation for the EDITOR, but they
6
+ // are Vite-coupled by construction (`/@fs/` dynamic imports of project files
7
+ // through the dev server, §0.1/§0.5 of the design doc) — unusable from a
8
+ // plain static build. This module mirrors their per-kind dispatch
9
+ // (`default-three`/`default-pixi`/`default-react`, `{ module }`, `{ ingest }`)
10
+ // but never imports a project file itself: the caller's OWN bundler already
11
+ // resolved whatever module graph the manifest's roots need, and hands the
12
+ // already-built pieces in via `entries` (keyed by world id) — no `/@fs/`, no
13
+ // dev server, no editor import.
14
+ //
15
+ // Engine-core react/pixi-free discipline (§0.6, mirrored from
16
+ // `create-runtime.ts`'s own documented rule for its `ReactRootAdapter`
17
+ // type-only import): this file never value-imports
18
+ // `pixi.js`, `react`, or `react-dom` — a
19
+ // canvas or react world's adapter is ALWAYS supplied already-constructed via
20
+ // `entries[id].adapter` (the caller's own module graph built it), so no
21
+ // surface's library is pulled into a game that has no world on it.
22
+
23
+ import { assertNever } from '@volter/editor-project/adapter/adapter-surface';
24
+ import type { RootDeclaration } from '@volter/editor-project/adapter/binding';
25
+ import { declaredRoots } from '@volter/editor-project/adapter/manifest-interpreter';
26
+ import type { RootAdapter } from '@volter/editor-project/adapter/root-adapter';
27
+ // Value-imported from the implementer's OWN path, not the type-only
28
+ // `../adapter` barrel (P-6).
29
+ import {
30
+ loadGameManifest,
31
+ type ResolvedAdapterRoot,
32
+ type ResolvedGameManifest,
33
+ } from '@volter/editor-project/manifest/load';
34
+ import { createGameRuntime, type GameSession, type RootMountSpec } from './create-runtime';
35
+ import { type DebugBridgeWindowTarget, maybeInstallDebugBridge } from './debug-bridge';
36
+ import { getDebugRegistry } from './debug-registry';
37
+ import { getGameplayRngTrapControl } from './gameplay-rng-trap';
38
+ import type { PlaytestContext } from './playtest';
39
+ import { RENDER_SEED_QUERY_PARAM } from './render-seed';
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // The `entries` contract
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /**
46
+ * A caller-supplied entry for a `kind: 'three'` world — two ways to satisfy
47
+ * it, matching the two shapes `resolveDefaultThreeAdapter` supports (an
48
+ * `entry`-module `setup`, or full caller control via a pre-built adapter):
49
+ *
50
+ * - `{ adapter }` — a fully-constructed {@link RootAdapter} (first-party or
51
+ * not). Always wins if present, regardless of the world's `entry` field —
52
+ * full caller control, including for a `{ module }`-adapter world
53
+ * (mountManifestRoots cannot import an arbitrary module path itself —
54
+ * for a default-exported world the editor builds the adapter from the
55
+ * entry module, `resolveR3FEntryAdapterForEditor`, and passes it here).
56
+ */
57
+ export interface ThreeMountEntry extends MountEntryDeclaration {
58
+ readonly kind: 'three';
59
+ readonly adapter?: RootAdapter | undefined;
60
+ }
61
+
62
+ /**
63
+ * The half of a root's binding declaration a CALLER holds — everything in
64
+ * `adapter/binding.ts`'s {@link RootDeclaration} except `root`, which this
65
+ * file already has from the manifest it just resolved.
66
+ *
67
+ * Optional throughout, and absent is the honest state for a caller who built
68
+ * an adapter by hand out of its own module graph: there is no entry namespace
69
+ * to report a static surface for, so the root registers with `binding: null`
70
+ * rather than one describing a module nobody loaded.
71
+ */
72
+ export interface MountEntryDeclaration {
73
+ readonly declaration?: Omit<RootDeclaration, 'root'> | undefined;
74
+ }
75
+
76
+ /**
77
+ * A caller-supplied entry for a `kind: 'canvas'` world — ALWAYS a
78
+ * fully-constructed `RootAdapter<'canvas'>`, built by the caller's own module
79
+ * graph. There is no "mountManifestRoots builds it for
80
+ * you" branch here — doing so would require this file to value-import
81
+ * `pixi.js`, which it deliberately never does (see this file's header
82
+ * comment).
83
+ */
84
+ export interface PixiMountEntry extends MountEntryDeclaration {
85
+ readonly kind: 'canvas';
86
+ readonly adapter: RootAdapter<'canvas'>;
87
+ }
88
+
89
+ /**
90
+ * A caller-supplied entry for a `kind: 'dom'` world — ALWAYS a
91
+ * fully-constructed `RootAdapter<'dom'>` (the caller's own module graph
92
+ * calls `createRoot(host.container).render(<Entry />)` itself). This file never value-imports `react`/`react-dom`
93
+ * itself (see this file's header comment) — building the adapter is
94
+ * entirely the caller's job.
95
+ */
96
+ export interface ReactMountEntry extends MountEntryDeclaration {
97
+ readonly kind: 'dom';
98
+ readonly adapter: RootAdapter<'dom'>;
99
+ }
100
+
101
+ export type MountEntry = ThreeMountEntry | PixiMountEntry | ReactMountEntry;
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Options / return shape
105
+ // ---------------------------------------------------------------------------
106
+
107
+ export interface MountManifestOptions {
108
+ /**
109
+ * A raw (unparsed) manifest value OR an already-`loadGameManifest`d
110
+ * {@link ResolvedGameManifest}. `mountManifestRoots` NEVER fetches or
111
+ * fs-reads the manifest itself (stays environment-neutral — Node CLI,
112
+ * browser, or headless test alike): the caller owns getting the JSON off
113
+ * disk/network (`loadGameManifestFile` for Node, a plain `fetch()` for a
114
+ * browser build) and may pass either the raw parsed JSON (run through the
115
+ * pure `loadGameManifest` here) or its own already-resolved manifest.
116
+ */
117
+ readonly manifest: unknown;
118
+ /** The host creates one absolutely-positioned surface per world inside
119
+ * this element — see `RootsRuntimeConfig.container`. */
120
+ readonly container: HTMLElement;
121
+ /** Caller-supplied, already-imported entries keyed by manifest world id.
122
+ * Optional overall — a manifest whose every world is scene-driven
123
+ * three with no custom component registry needs none at all. */
124
+ readonly entries?: Readonly<Record<string, MountEntry>> | undefined;
125
+ /** Defaults to the manifest's own `resolution` field if declared, else
126
+ * the container's own size (`RootsRuntimeConfig`'s existing default). */
127
+ readonly width?: number | undefined;
128
+ readonly height?: number | undefined;
129
+ /** Forwarded to `createGameRuntime` — Node/headless test harnesses only,
130
+ * never a real host. See `RootsRuntimeConfig.headless`. */
131
+ readonly headless?: boolean | undefined;
132
+ /**
133
+ * Task 2.1: overrides for the `?vgai-debug=1` bridge `mountManifestRoots`
134
+ * installs at the tail of every mount — the engine-owned install point so
135
+ * every standalone project gets it with zero template edits. Omit both in
136
+ * a real host (defaults to `window.location`/ `window`); a headless test
137
+ * supplies fakes here instead of touching the global object, mirroring
138
+ * `render-control.ts`'s own `location`/`target` override precedent.
139
+ */
140
+ readonly debugBridge?:
141
+ | {
142
+ readonly url?: { readonly search: string } | undefined;
143
+ readonly window?: DebugBridgeWindowTarget | undefined;
144
+ }
145
+ | undefined;
146
+ /**
147
+ * D15 (T-D15.1) — the "explicit config" leg of the boot-time seed
148
+ * precedence (`manifest.determinism.defaultSeed` → `?vgai-seed=` → this),
149
+ * highest-precedence, for a caller that already knows the exact seed it
150
+ * wants (a future CLI `--seed`/probe fixture `seed` option, T-D15.6).
151
+ * Ignored entirely unless the manifest declares
152
+ * `determinism.seededRandom` — see `resolveDeterminismSeed` below.
153
+ */
154
+ readonly seed?: number | undefined;
155
+ /** Host identity for a private play run or coordinated Team Test. */
156
+ readonly playtest?: PlaytestContext | null | undefined;
157
+ /** Where to read `?vgai-seed=` from for the boot-time seed reader. Same
158
+ * override precedent as `debugBridge.url` (defaults to `window.location`
159
+ * when a real `window` exists; a headless caller with no override gets no
160
+ * query-param seed, same as `maybeInstallDebugBridge`'s own url default). */
161
+ readonly seedUrl?: { readonly search: string } | undefined;
162
+ }
163
+
164
+ /**
165
+ * D15 (T-D15.1) boot-time seed reader — resolves the root seed
166
+ * `mountManifestRoots` threads into `createGameRuntime` (and therefore
167
+ * `createGame`, BEFORE any world's `mount()`/`setup()` runs). Returns
168
+ * `undefined` when the manifest doesn't declare `determinism.seededRandom`
169
+ * at all — `?vgai-seed=` and `defaultSeed` are both ignored in that case
170
+ * (§2.a: "the mount path seeds ctx.random ... iff declared"; an undeclared
171
+ * project's `ctx.random` still exists, just boots from `createGame`'s own
172
+ * fixed default, unaffected by the manifest or the URL).
173
+ *
174
+ * Precedence when declared (highest wins): `explicitSeed` (a caller-supplied
175
+ * config value) → `?vgai-seed=<int>` on `url` → `manifest.determinism
176
+ * .defaultSeed`.
177
+ */
178
+ export function resolveDeterminismSeed(opts: {
179
+ readonly determinism: ResolvedGameManifest['determinism'];
180
+ readonly explicitSeed: number | undefined;
181
+ readonly url: { readonly search: string } | undefined;
182
+ }): number | undefined {
183
+ if (opts.determinism?.seededRandom !== true) return undefined;
184
+ const querySeed = opts.url ? readSeedQueryParam(opts.url) : undefined;
185
+ return opts.explicitSeed ?? querySeed ?? opts.determinism.defaultSeed;
186
+ }
187
+
188
+ function readSeedQueryParam(url: { readonly search: string }): number | undefined {
189
+ const raw = new URLSearchParams(url.search).get(RENDER_SEED_QUERY_PARAM);
190
+ if (raw === null || raw === '') return undefined;
191
+ const parsed = Number(raw);
192
+ return Number.isFinite(parsed) ? parsed : undefined;
193
+ }
194
+
195
+ function hasRealWindow(): boolean {
196
+ return typeof window !== 'undefined';
197
+ }
198
+
199
+ // `mountManifestRoots` returns the SAME `GameSession` shape
200
+ // `createGameRuntime` returns (D-Z2: "the same session shape... or a thin
201
+ // superset") — callers keep exactly one teardown path (`session.stop()`)
202
+ // whether they mounted through `createGameRuntime` directly or through this
203
+ // helper.
204
+ export type MountedManifestSession = GameSession;
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Raw-vs-resolved manifest detection
208
+ // ---------------------------------------------------------------------------
209
+
210
+ /**
211
+ * Distinguish an already-`loadGameManifest`d {@link ResolvedGameManifest}
212
+ * from a raw (pre-Zod-parse) manifest value. The two shapes differ in
213
+ * exactly one load-bearing way for this check: a `ResolvedAdapterRoot.adapter`
214
+ * is `{ type: 'default' | 'module' | 'ingest', identity, ... }`, while a raw
215
+ * `AdapterRoot.adapter` is the literal string `'default'` or a bare `{module}`/
216
+ * `{ingest}` object with NO `type`/`identity` fields (`schema.ts`'s
217
+ * `RootAdapterSchema`). An empty `roots` array is treated as "not resolved"
218
+ * (falls through to `loadGameManifest`, whose own Zod validation reports the
219
+ * empty-array error, if any, more precisely than a guess here could).
220
+ */
221
+ function looksAlreadyResolved(value: unknown): value is ResolvedGameManifest {
222
+ if (typeof value !== 'object' || value === null) return false;
223
+ // The loader's own output field is the surest mark: a raw manifest never
224
+ // carries it (the schema is strict). Without this, a RESOLVED manifest
225
+ // with `roots: []` — a models project, nothing to play — fell through to
226
+ // `loadGameManifest`, whose strict parse refused THIS key ("Unrecognized
227
+ // key: deferredConfigurationKinds") instead of saying "no roots"
228
+ // (blind lantern round 21, 2026-09-06, sent there by the never-played nag).
229
+ if (
230
+ Array.isArray((value as { deferredConfigurationKinds?: unknown }).deferredConfigurationKinds)
231
+ ) {
232
+ return true;
233
+ }
234
+ const roots = (value as { roots?: unknown }).roots;
235
+ if (!Array.isArray(roots) || roots.length === 0) return false;
236
+ return roots.every((world) => {
237
+ if (typeof world !== 'object' || world === null) return false;
238
+ const adapter = (world as { adapter?: unknown }).adapter;
239
+ if (typeof adapter !== 'object' || adapter === null) return false;
240
+ const type = (adapter as { type?: unknown }).type;
241
+ return type === 'builtin' || type === 'module' || type === 'ingest';
242
+ });
243
+ }
244
+
245
+ /** The manifest resolved, whether `raw` is the file's JSON or an already-resolved manifest. */
246
+ export function resolveManifest(raw: unknown): ResolvedGameManifest {
247
+ if (looksAlreadyResolved(raw)) return raw;
248
+ return loadGameManifest(raw, { configurationKinds: 'defer' });
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Per-kind entry -> adapter resolution (mirrors `resolveRootBinding`'s
253
+ // per-identity dispatch, `packages/editor/src/binding-resolver.ts`)
254
+ // ---------------------------------------------------------------------------
255
+
256
+ function resolveThreeAdapter(
257
+ world: ResolvedAdapterRoot,
258
+ entry: ThreeMountEntry | undefined,
259
+ ): RootAdapter {
260
+ if (entry?.adapter) return entry.adapter;
261
+
262
+ if (world.adapter.type === 'module') {
263
+ throw new Error(
264
+ `mountManifestRoots: world "${world.id}" (three) declares a { module } adapter ` +
265
+ `("${world.adapter.module}") — mountManifestRoots never imports an arbitrary module ` +
266
+ 'path itself (no `/@fs/`, no dev server). Import the module yourself and supply the ' +
267
+ `constructed adapter via entries["${world.id}"] = { kind: 'three', adapter }.`,
268
+ );
269
+ }
270
+ if (world.adapter.type === 'ingest') {
271
+ throw new Error(
272
+ `mountManifestRoots: world "${world.id}" (three) declares an { ingest } adapter — ` +
273
+ "captured ingest roots require the editor's dev-server-backed mount machinery (an EditorStore " +
274
+ 'plus in-realm scene capture, see resolveIngestDescriptor + ' +
275
+ 'mountThreeIngestRootFromManifest) and are ' +
276
+ 'not supported by mountManifestRoots (no porting aids, no hosted ingest routes).',
277
+ );
278
+ }
279
+
280
+ // type === 'default'
281
+ if (world.entry !== undefined) {
282
+ throw new Error(
283
+ `mountManifestRoots: world "${world.id}" (three) declares \`entry\` "${world.entry}" ` +
284
+ `— supply entries["${world.id}"] = { kind: 'three', adapter } (for a default-exported ` +
285
+ 'world, the editor builds it from the entry module with ' +
286
+ '`resolveR3FEntryAdapterForEditor`). Got ' +
287
+ `${entry === undefined ? 'no entry at all' : 'an entry with no `adapter`'}.`,
288
+ );
289
+ }
290
+ // Unreachable in practice: `load.ts`'s `checkAdapterSceneEntryRules` requires
291
+ // `entry` for every 'default' THREE adapter world at manifest-load time (and
292
+ // rejects any other content declaration on one outright).
293
+ throw new Error(
294
+ `mountManifestRoots: world "${world.id}" is default-three with no \`entry\` — this ` +
295
+ 'should have been rejected by manifest validation.',
296
+ );
297
+ }
298
+
299
+ function resolvePixiAdapter(
300
+ world: ResolvedAdapterRoot,
301
+ entry: PixiMountEntry | undefined,
302
+ ): RootAdapter<'canvas'> {
303
+ if (entry?.adapter) return entry.adapter;
304
+ throw new Error(
305
+ `mountManifestRoots: world "${world.id}" (canvas) has no entries["${world.id}"] — a canvas ` +
306
+ 'world always needs a caller-supplied, already-constructed adapter ' +
307
+ `(entries["${world.id}"] = { kind: 'canvas', adapter }, constructed from your own ` +
308
+ 'already-imported pixi module graph) — mountManifestRoots never ' +
309
+ "value-imports pixi.js (mirrors create-runtime.ts's own pixi-free-core discipline).",
310
+ );
311
+ }
312
+
313
+ function resolveReactAdapter(
314
+ world: ResolvedAdapterRoot,
315
+ entry: ReactMountEntry | undefined,
316
+ ): RootAdapter<'dom'> {
317
+ if (entry?.adapter) return entry.adapter;
318
+ throw new Error(
319
+ `mountManifestRoots: world "${world.id}" (react) has no entries["${world.id}"] — a react ` +
320
+ "world always needs a caller-supplied, already-constructed `RootAdapter<'dom'>` " +
321
+ `(entries["${world.id}"] = { kind: 'dom', adapter }, mounting via your own already-imported ` +
322
+ 'react-dom `createRoot`) — mountManifestRoots never ' +
323
+ "value-imports react/react-dom (mirrors create-runtime.ts's own react-free-core discipline).",
324
+ );
325
+ }
326
+
327
+ function buildRootMountSpec(
328
+ world: ResolvedAdapterRoot,
329
+ entry: MountEntry | undefined,
330
+ ): RootMountSpec {
331
+ if (entry !== undefined && entry.kind !== world.surface) {
332
+ throw new Error(
333
+ `mountManifestRoots: entries["${world.id}"] declares kind "${entry.kind}" but the ` +
334
+ `manifest's root "${world.id}" uses surface "${world.surface}" — fix the entries key (or the ` +
335
+ 'manifest) so the two agree.',
336
+ );
337
+ }
338
+
339
+ const base = {
340
+ id: world.id,
341
+ zOrder: world.zOrder,
342
+ pausable: world.pausable,
343
+ // `root` is this file's own resolved manifest record; the rest — the
344
+ // parsed `vgai.adapter.ts`, the entry namespace, the entry's declared
345
+ // debug/systems bindings — only the caller who loaded the entry can know.
346
+ ...(entry?.declaration === undefined
347
+ ? {}
348
+ : { declaration: { root: world, ...entry.declaration } }),
349
+ };
350
+
351
+ // `entry`'s `kind` is guaranteed to agree with `world.kind` past the guard
352
+ // above (or `entry` is `undefined`) — TS can't narrow a `Record` lookup
353
+ // through that runtime check, so each branch casts to its own entry shape;
354
+ // the actual safety comes from the mismatch guard, not from the cast.
355
+ if (world.surface === 'three') {
356
+ const threeEntry = entry as ThreeMountEntry | undefined;
357
+ return { ...base, kind: 'three', adapter: resolveThreeAdapter(world, threeEntry) };
358
+ }
359
+ if (world.surface === 'canvas') {
360
+ const pixiEntry = entry as PixiMountEntry | undefined;
361
+ return { ...base, kind: 'canvas', adapter: resolvePixiAdapter(world, pixiEntry) };
362
+ }
363
+ if (world.surface === 'dom') {
364
+ const reactEntry = entry as ReactMountEntry | undefined;
365
+ return { ...base, kind: 'dom', adapter: resolveReactAdapter(world, reactEntry) };
366
+ }
367
+ // Exhaustiveness guard (§7.4-2, same idiom as `resolveAllRoots`'s own
368
+ // dispatch loop): `world.kind` is the closed `AdapterRoot['kind']` union
369
+ // (`z.enum(['three','canvas','react'])`), so a hypothetical 4th kind
370
+ // must fail to compile here, not silently fall through.
371
+ return assertNever(world.surface, 'mountManifestRoots');
372
+ }
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // Entry point
376
+ // ---------------------------------------------------------------------------
377
+
378
+ /**
379
+ * Mount every world declared by a `vgai.project.json` manifest onto `container`,
380
+ * standalone — no editor, no dev server. The generalization of
381
+ * `examples/tri-world/src/main.ts`'s hand-built `RootMountSpec[]` (D-Z2). Reuses
382
+ * the pure `loadGameManifest` + `createGameRuntime({ roots })` and mirrors
383
+ * `resolveAllRoots`'s per-kind dispatch — but every entry the manifest needs
384
+ * beyond what a `default`- adapter scene/entry can express on its own comes from
385
+ * the caller's OWN already-imported module graph (`opts.entries`), never a
386
+ * dynamic import this file performs itself.
387
+ *
388
+ * Degrades loudly: a missing entry, a kind/shape mismatch, a `{ module }`/`{
389
+ * ingest }` adapter with no caller-supplied adapter, or an empty manifest all
390
+ * throw a named `Error` identifying the world id, its kind, and what was
391
+ * expected — never a silent skip or a partially-mounted session.
392
+ */
393
+ export async function mountManifestRoots(opts: MountManifestOptions): Promise<GameSession> {
394
+ const manifest = resolveManifest(opts.manifest);
395
+ const mountPlan = declaredRoots(manifest);
396
+ if (mountPlan.length === 0) {
397
+ throw new Error('mountManifestRoots: manifest declares no roots — nothing to mount.');
398
+ }
399
+ const entries = opts.entries ?? {};
400
+
401
+ const roots: RootMountSpec[] = mountPlan.map((world) =>
402
+ buildRootMountSpec(world, entries[world.id]),
403
+ );
404
+
405
+ // D15 (T-D15.1) — resolved BEFORE `createGameRuntime` (which constructs the
406
+ // Game, and therefore `ctx.random`, before any world's `mount()`/`setup()`
407
+ // runs — see `createGame`'s own doc comment): the seed must be baked in at
408
+ // construction time, not patched in afterward, or a world's `setup()`
409
+ // would already have drawn from the wrong (default) sequence before any
410
+ // post-hoc reseed could take effect.
411
+ const seedUrl = opts.seedUrl ?? (hasRealWindow() ? window.location : undefined);
412
+ const resolvedSeed = resolveDeterminismSeed({
413
+ determinism: manifest.determinism,
414
+ explicitSeed: opts.seed,
415
+ url: seedUrl,
416
+ });
417
+
418
+ const session = await createGameRuntime({
419
+ container: opts.container,
420
+ roots,
421
+ width: opts.width ?? manifest.resolution?.width,
422
+ height: opts.height ?? manifest.resolution?.height,
423
+ headless: opts.headless,
424
+ // `rendering.antialias` reaches the WebGL context at CONSTRUCTION and can be honoured
425
+ // nowhere else — see the manifest schema's own `rendering` block and
426
+ // `adapter/renderer-config.ts`'s header for why it is not a world-level declaration.
427
+ ...(manifest.rendering === undefined ? {} : { antialias: manifest.rendering.antialias }),
428
+ seed: resolvedSeed,
429
+ playtest: opts.playtest,
430
+ });
431
+
432
+ // D15 (T-D15.3) — the dev-mode Math.random phase trap: only while the
433
+ // manifest declares the contract AND this is a dev build (mirrors D18's
434
+ // debug-bridge gate — `import.meta.env.DEV`, never a production build
435
+ // unless something ELSE explicitly opts in, which this trap has no
436
+ // opt-in for at all: it is dev-only, full stop). `getGameplayRngTrapControl`
437
+ // returns `null` only for a bare `Game`-shaped test stand-in predating
438
+ // T7.1 — real mounts always have one (same precedent as `getDebugRegistry`
439
+ // just below).
440
+ if (manifest.determinism?.seededRandom === true && Boolean(import.meta.env?.DEV)) {
441
+ getGameplayRngTrapControl(session.game)?.setEnabled(true);
442
+ }
443
+
444
+ // Task 2.1 — install the `?vgai-debug=1` bridge at the TAIL of every
445
+ // manifest mount (engine-owned, so a standalone project gets it with zero
446
+ // template edits). `getDebugRegistry` returns `null` only for a bare
447
+ // `Game`-shaped test stand-in predating T7.1 — real mounts always have one.
448
+ const debugRegistry = getDebugRegistry(session.game);
449
+ if (debugRegistry) {
450
+ // Defect 3 fix: `setRoomDeclared` (the locus-required rule — a project
451
+ // with a Colyseus room must declare `locus: 'client' | 'server'` on every
452
+ // debug command) was never actually called from a real mount path — this
453
+ // is that wiring. `manifest.server` (`ResolvedGameManifest.server`,
454
+ // `manifest/load.ts`) is `{ room, module } | undefined`; its mere presence
455
+ // is the "this project declares a room" signal, independent of
456
+ // whether/when the game actually joins it.
457
+ if (manifest.configurations.some((configuration) => configuration.kind === 'process')) {
458
+ debugRegistry.setRoomDeclared(true);
459
+ }
460
+ // Defect 5 fix: `maybeInstallDebugBridge` adds `window` listeners and
461
+ // publishes `window.__vgai` with no way to undo either — wire its
462
+ // `uninstall()` into THIS session's own `stop()` so a caller that tears
463
+ // this mount down doesn't leave the bridge (and its listeners) live.
464
+ const bridgeHandle = maybeInstallDebugBridge({
465
+ registry: debugRegistry,
466
+ manifest,
467
+ url: opts.debugBridge?.url,
468
+ window: opts.debugBridge?.window,
469
+ });
470
+ if (bridgeHandle) {
471
+ const stopSession = session.stop;
472
+ session.stop = () => {
473
+ bridgeHandle.uninstall();
474
+ stopSession();
475
+ };
476
+ }
477
+ }
478
+
479
+ return session;
480
+ }