@vgai/engine 0.5.21 → 0.5.23

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 (339) hide show
  1. package/README.md +6 -3
  2. package/dist/adapter/adapter-module.d.ts.map +1 -1
  3. package/dist/adapter/adapter-module.js +2 -4
  4. package/dist/adapter/adapter-surface.d.ts +1 -1
  5. package/dist/adapter/adapter-surface.js +1 -1
  6. package/dist/adapter/authoring.d.ts +19 -21
  7. package/dist/adapter/authoring.d.ts.map +1 -1
  8. package/dist/adapter/authoring.js +2 -2
  9. package/dist/adapter/binding.d.ts +341 -0
  10. package/dist/adapter/binding.d.ts.map +1 -0
  11. package/dist/adapter/binding.js +148 -0
  12. package/dist/adapter/colyseus-networking-adapter.d.ts.map +1 -1
  13. package/dist/adapter/colyseus-networking-adapter.js +23 -4
  14. package/dist/adapter/entrypoint-selection-readers.d.ts +24 -0
  15. package/dist/adapter/entrypoint-selection-readers.d.ts.map +1 -0
  16. package/dist/adapter/entrypoint-selection-readers.js +63 -0
  17. package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -1
  18. package/dist/adapter/entrypoint-selection-source.js +1 -44
  19. package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
  20. package/dist/adapter/finders/scenes-from-entrypoint-selection.js +1 -52
  21. package/dist/adapter/host-context.d.ts +46 -15
  22. package/dist/adapter/host-context.d.ts.map +1 -1
  23. package/dist/adapter/host-context.js +2 -9
  24. package/dist/adapter/index.d.ts +11 -12
  25. package/dist/adapter/index.d.ts.map +1 -1
  26. package/dist/adapter/index.js +9 -10
  27. package/dist/adapter/ingest/game-contract.d.ts +0 -3
  28. package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
  29. package/dist/adapter/ingest/game-contract.js +0 -3
  30. package/dist/adapter/ingest/scene-capture.d.ts +19 -23
  31. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  32. package/dist/adapter/ingest/scene-capture.js +143 -17
  33. package/dist/adapter/ingest/visible-capture-window.d.ts +34 -0
  34. package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
  35. package/dist/adapter/manifest-interpreter.d.ts +59 -0
  36. package/dist/adapter/manifest-interpreter.d.ts.map +1 -0
  37. package/dist/adapter/manifest-interpreter.js +50 -0
  38. package/dist/adapter/native-debug-module.d.ts +71 -3
  39. package/dist/adapter/native-debug-module.d.ts.map +1 -1
  40. package/dist/adapter/native-debug-module.js +146 -10
  41. package/dist/adapter/rapier-physics-adapter.d.ts +31 -1
  42. package/dist/adapter/rapier-physics-adapter.d.ts.map +1 -1
  43. package/dist/adapter/rapier-physics-adapter.js +50 -36
  44. package/dist/adapter/renderer-config.d.ts +3 -3
  45. package/dist/adapter/renderer-config.js +3 -3
  46. package/dist/adapter/root-adapter.d.ts +7 -7
  47. package/dist/adapter/root-adapter.js +4 -4
  48. package/dist/adapter/system-adapter.d.ts +45 -7
  49. package/dist/adapter/system-adapter.d.ts.map +1 -1
  50. package/dist/adapter/system-seam-contract.d.ts +3 -3
  51. package/dist/adapter/system-seam-contract.js +3 -3
  52. package/dist/adapter/system-slot.d.ts +168 -0
  53. package/dist/adapter/system-slot.d.ts.map +1 -0
  54. package/dist/adapter/system-slot.js +229 -0
  55. package/dist/adapter/transform.d.ts +11 -1
  56. package/dist/adapter/transform.d.ts.map +1 -1
  57. package/dist/ai/navigation.d.ts +17 -1
  58. package/dist/ai/navigation.d.ts.map +1 -1
  59. package/dist/ai/navigation.js +26 -5
  60. package/dist/animation/anim-graph-types.d.ts +11 -3
  61. package/dist/animation/anim-graph-types.d.ts.map +1 -1
  62. package/dist/animation/theatre-clock-binding.d.ts +4 -4
  63. package/dist/animation/theatre-clock-binding.js +4 -4
  64. package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
  65. package/dist/animation/xstate-animation-binding.js +2 -2
  66. package/dist/animation/xstate-animation-meta.d.ts +13 -0
  67. package/dist/animation/xstate-animation-meta.d.ts.map +1 -1
  68. package/dist/animation/xstate-animation-meta.js +14 -0
  69. package/dist/asset-formats/index.d.ts +4 -7
  70. package/dist/asset-formats/index.d.ts.map +1 -1
  71. package/dist/asset-formats/index.js +4 -7
  72. package/dist/asset-formats/render-env.d.ts +5 -8
  73. package/dist/asset-formats/render-env.d.ts.map +1 -1
  74. package/dist/asset-formats/render-env.js +3 -6
  75. package/dist/asset-parse-error.d.ts +2 -5
  76. package/dist/asset-parse-error.d.ts.map +1 -1
  77. package/dist/asset-parse-error.js +2 -5
  78. package/dist/audio/bus-mixer.d.ts +101 -0
  79. package/dist/audio/bus-mixer.d.ts.map +1 -0
  80. package/dist/audio/bus-mixer.js +115 -0
  81. package/dist/canvas-react/index.d.ts +5 -12
  82. package/dist/canvas-react/index.d.ts.map +1 -1
  83. package/dist/canvas-react/index.js +5 -12
  84. package/dist/canvas-react/pixi-react-root-factory.d.ts +34 -21
  85. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
  86. package/dist/canvas-react/pixi-react-root-factory.js +274 -36
  87. package/dist/core/countdown-timer.d.ts +118 -0
  88. package/dist/core/countdown-timer.d.ts.map +1 -0
  89. package/dist/core/countdown-timer.js +137 -0
  90. package/dist/core/deferred-commands.d.ts +127 -0
  91. package/dist/core/deferred-commands.d.ts.map +1 -0
  92. package/dist/core/deferred-commands.js +132 -0
  93. package/dist/core/frame-pacing.d.ts +1 -1
  94. package/dist/core/frame-pacing.js +2 -2
  95. package/dist/core/game-loop.d.ts +1 -3
  96. package/dist/core/game-loop.d.ts.map +1 -1
  97. package/dist/core/game-loop.js +1 -3
  98. package/dist/core/seeded-random.d.ts +1 -1
  99. package/dist/core/sim-clock.d.ts +19 -8
  100. package/dist/core/sim-clock.d.ts.map +1 -1
  101. package/dist/core/sim-clock.js +18 -7
  102. package/dist/core/types.d.ts +1 -3
  103. package/dist/core/types.d.ts.map +1 -1
  104. package/dist/defaults.d.ts +6 -14
  105. package/dist/defaults.d.ts.map +1 -1
  106. package/dist/defaults.js +6 -14
  107. package/dist/dev/instruments.d.ts +38 -11
  108. package/dist/dev/instruments.d.ts.map +1 -1
  109. package/dist/dev/instruments.js +81 -11
  110. package/dist/dev/register-render-vitals.d.ts +1 -4
  111. package/dist/dev/register-render-vitals.d.ts.map +1 -1
  112. package/dist/dev/register-render-vitals.js +1 -4
  113. package/dist/dev/render-debug-adapter.d.ts +2 -3
  114. package/dist/dev/render-debug-adapter.d.ts.map +1 -1
  115. package/dist/dev/render-debug-adapter.js +2 -3
  116. package/dist/dev/render-vitals.d.ts +3 -3
  117. package/dist/dev/render-vitals.js +3 -3
  118. package/dist/dev/webgl-frame-capture.d.ts +1 -1
  119. package/dist/dev/webgl-frame-capture.js +1 -1
  120. package/dist/index.d.ts +2 -5
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +2 -4
  123. package/dist/input/host-pointer.d.ts +122 -0
  124. package/dist/input/host-pointer.d.ts.map +1 -0
  125. package/dist/input/host-pointer.js +160 -0
  126. package/dist/input/input-manager.d.ts +33 -5
  127. package/dist/input/input-manager.d.ts.map +1 -1
  128. package/dist/input/input-manager.js +48 -13
  129. package/dist/input/rebind-controller.d.ts.map +1 -1
  130. package/dist/input/rebind-controller.js +6 -8
  131. package/dist/input/schema.d.ts.map +1 -1
  132. package/dist/input/schema.js +56 -28
  133. package/dist/manifest/filename.d.ts.map +1 -1
  134. package/dist/manifest/filename.js +8 -12
  135. package/dist/manifest/load.d.ts +10 -10
  136. package/dist/manifest/load.d.ts.map +1 -1
  137. package/dist/manifest/load.js +9 -6
  138. package/dist/manifest/schema.d.ts +17 -21
  139. package/dist/manifest/schema.d.ts.map +1 -1
  140. package/dist/manifest/schema.js +45 -35
  141. package/dist/physics/collider-dimensions.d.ts +8 -12
  142. package/dist/physics/collider-dimensions.d.ts.map +1 -1
  143. package/dist/physics/collider-dimensions.js +8 -12
  144. package/dist/pixi/scene-capture.d.ts +11 -13
  145. package/dist/pixi/scene-capture.d.ts.map +1 -1
  146. package/dist/react/world-state.d.ts +1 -1
  147. package/dist/react/world-state.d.ts.map +1 -1
  148. package/dist/render/directional-shadow-fit.d.ts +88 -0
  149. package/dist/render/directional-shadow-fit.d.ts.map +1 -0
  150. package/dist/render/directional-shadow-fit.js +133 -0
  151. package/dist/render/environment-capture.d.ts +68 -0
  152. package/dist/render/environment-capture.d.ts.map +1 -0
  153. package/dist/render/environment-capture.js +90 -0
  154. package/dist/render/ibl-override-material.d.ts +97 -0
  155. package/dist/render/ibl-override-material.d.ts.map +1 -0
  156. package/dist/render/ibl-override-material.js +114 -0
  157. package/dist/render/light-camera-factory.d.ts +5 -7
  158. package/dist/render/light-camera-factory.d.ts.map +1 -1
  159. package/dist/render/light-camera-factory.js +8 -11
  160. package/dist/render/material-factory.js +1 -1
  161. package/dist/render/render-batch-system.d.ts.map +1 -1
  162. package/dist/render/render-features.d.ts +17 -6
  163. package/dist/render/render-features.d.ts.map +1 -1
  164. package/dist/render/render-features.js +0 -28
  165. package/dist/runtime/create-runtime.d.ts +34 -25
  166. package/dist/runtime/create-runtime.d.ts.map +1 -1
  167. package/dist/runtime/create-runtime.js +218 -93
  168. package/dist/runtime/debug-registry.d.ts +77 -8
  169. package/dist/runtime/debug-registry.d.ts.map +1 -1
  170. package/dist/runtime/debug-registry.js +3 -3
  171. package/dist/runtime/dev-build.d.ts +2 -2
  172. package/dist/runtime/dev-build.js +2 -2
  173. package/dist/runtime/game-input-seams.d.ts +51 -0
  174. package/dist/runtime/game-input-seams.d.ts.map +1 -0
  175. package/dist/runtime/game-input-seams.js +86 -0
  176. package/dist/runtime/game.d.ts +104 -40
  177. package/dist/runtime/game.d.ts.map +1 -1
  178. package/dist/runtime/game.js +119 -61
  179. package/dist/runtime/mount-game.d.ts +0 -16
  180. package/dist/runtime/mount-game.d.ts.map +1 -1
  181. package/dist/runtime/mount-game.js +26 -41
  182. package/dist/runtime/mount-manifest.d.ts +22 -14
  183. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  184. package/dist/runtime/mount-manifest.js +22 -20
  185. package/dist/runtime/playtest.d.ts +22 -0
  186. package/dist/runtime/playtest.d.ts.map +1 -0
  187. package/dist/runtime/playtest.js +12 -0
  188. package/dist/runtime/state-bridge.d.ts +2 -3
  189. package/dist/runtime/state-bridge.d.ts.map +1 -1
  190. package/dist/runtime/state-bridge.js +1 -0
  191. package/dist/setup/setup-audio.d.ts +12 -0
  192. package/dist/setup/setup-audio.d.ts.map +1 -1
  193. package/dist/setup/setup-audio.js +9 -10
  194. package/dist/setup/setup-renderer.d.ts +2 -25
  195. package/dist/setup/setup-renderer.d.ts.map +1 -1
  196. package/dist/setup/setup-renderer.js +2 -31
  197. package/dist/world3d-react/index.d.ts +15 -25
  198. package/dist/world3d-react/index.d.ts.map +1 -1
  199. package/dist/world3d-react/index.js +15 -25
  200. package/dist/world3d-react/r3f-root-factory.d.ts +28 -34
  201. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  202. package/dist/world3d-react/r3f-root-factory.js +403 -57
  203. package/dist/world3d-react/rapier-physics-bridge.d.ts +30 -17
  204. package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
  205. package/dist/world3d-react/rapier-physics-bridge.js +143 -58
  206. package/package.json +1 -1
  207. package/schemas/engine-capabilities.json +16 -14
  208. package/schemas/vgai-project.schema.json +35 -26
  209. package/src/adapter/adapter-module.ts +2 -4
  210. package/src/adapter/adapter-surface.ts +1 -1
  211. package/src/adapter/authoring.ts +19 -21
  212. package/src/adapter/binding.ts +468 -0
  213. package/src/adapter/colyseus-networking-adapter.ts +20 -5
  214. package/src/adapter/entrypoint-selection-readers.ts +66 -0
  215. package/src/adapter/entrypoint-selection-source.ts +1 -40
  216. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +1 -48
  217. package/src/adapter/host-context.ts +45 -15
  218. package/src/adapter/index.ts +23 -11
  219. package/src/adapter/ingest/game-contract.ts +0 -3
  220. package/src/adapter/ingest/scene-capture.ts +190 -39
  221. package/src/adapter/ingest/visible-capture-window.ts +35 -0
  222. package/src/adapter/manifest-interpreter.ts +64 -0
  223. package/src/adapter/native-debug-module.ts +248 -15
  224. package/src/adapter/rapier-physics-adapter.ts +76 -20
  225. package/src/adapter/renderer-config.ts +3 -3
  226. package/src/adapter/root-adapter.ts +7 -7
  227. package/src/adapter/system-adapter.ts +43 -8
  228. package/src/adapter/system-seam-contract.ts +3 -3
  229. package/src/adapter/system-slot.ts +291 -0
  230. package/src/adapter/transform.ts +18 -1
  231. package/src/ai/navigation.ts +30 -3
  232. package/src/animation/anim-graph-types.ts +11 -3
  233. package/src/animation/theatre-clock-binding.ts +4 -4
  234. package/src/animation/xstate-animation-binding.ts +2 -2
  235. package/src/animation/xstate-animation-meta.ts +16 -0
  236. package/src/asset-formats/index.ts +4 -7
  237. package/src/asset-formats/render-env.ts +3 -6
  238. package/src/asset-parse-error.ts +2 -5
  239. package/src/audio/bus-mixer.ts +161 -0
  240. package/src/canvas-react/index.ts +5 -21
  241. package/src/canvas-react/pixi-react-root-factory.tsx +301 -39
  242. package/src/core/countdown-timer.ts +188 -0
  243. package/src/core/deferred-commands.ts +174 -0
  244. package/src/core/frame-pacing.ts +2 -2
  245. package/src/core/game-loop.ts +1 -3
  246. package/src/core/seeded-random.ts +1 -1
  247. package/src/core/sim-clock.ts +19 -8
  248. package/src/core/types.ts +1 -3
  249. package/src/defaults.ts +6 -14
  250. package/src/dev/instruments.ts +108 -11
  251. package/src/dev/register-render-vitals.ts +1 -4
  252. package/src/dev/render-debug-adapter.ts +2 -3
  253. package/src/dev/render-vitals.ts +3 -3
  254. package/src/dev/webgl-frame-capture.ts +1 -1
  255. package/src/index.ts +2 -5
  256. package/src/input/host-pointer.ts +230 -0
  257. package/src/input/input-manager.ts +49 -13
  258. package/src/input/input-types.ts +1 -1
  259. package/src/input/rebind-controller.ts +6 -8
  260. package/src/input/schema.ts +216 -188
  261. package/src/manifest/filename.ts +8 -12
  262. package/src/manifest/load.ts +17 -13
  263. package/src/manifest/schema.ts +55 -47
  264. package/src/physics/collider-dimensions.ts +8 -12
  265. package/src/pixi/scene-capture.ts +12 -15
  266. package/src/react/world-state.tsx +1 -1
  267. package/src/render/directional-shadow-fit.ts +156 -0
  268. package/src/render/environment-capture.ts +102 -0
  269. package/src/render/ibl-override-material.ts +170 -0
  270. package/src/render/light-camera-factory.ts +8 -11
  271. package/src/render/material-factory.ts +1 -1
  272. package/src/render/render-batch-system.ts +14 -6
  273. package/src/render/render-features.ts +17 -35
  274. package/src/runtime/create-runtime.ts +270 -108
  275. package/src/runtime/debug-registry.ts +84 -8
  276. package/src/runtime/dev-build.ts +2 -2
  277. package/src/runtime/game-input-seams.ts +108 -0
  278. package/src/runtime/game.ts +231 -85
  279. package/src/runtime/mount-game.ts +31 -57
  280. package/src/runtime/mount-manifest.ts +47 -36
  281. package/src/runtime/playtest.ts +22 -0
  282. package/src/runtime/state-bridge.ts +3 -3
  283. package/src/setup/setup-audio.ts +21 -11
  284. package/src/setup/setup-renderer.ts +2 -61
  285. package/src/world3d-react/index.ts +15 -31
  286. package/src/world3d-react/r3f-root-factory.tsx +451 -60
  287. package/src/world3d-react/rapier-physics-bridge.tsx +154 -58
  288. package/dist/adapter/setup-three-root-adapter.d.ts +0 -86
  289. package/dist/adapter/setup-three-root-adapter.d.ts.map +0 -1
  290. package/dist/adapter/setup-three-root-adapter.js +0 -908
  291. package/dist/animation/clip-map.d.ts +0 -12
  292. package/dist/animation/clip-map.d.ts.map +0 -1
  293. package/dist/animation/clip-map.js +0 -31
  294. package/dist/asset-registry.d.ts +0 -38
  295. package/dist/asset-registry.d.ts.map +0 -1
  296. package/dist/asset-registry.js +0 -66
  297. package/dist/canvas-react/engine-bridge.d.ts +0 -45
  298. package/dist/canvas-react/engine-bridge.d.ts.map +0 -1
  299. package/dist/canvas-react/engine-bridge.js +0 -45
  300. package/dist/canvas-react/pixi-react-adapter.d.ts +0 -77
  301. package/dist/canvas-react/pixi-react-adapter.d.ts.map +0 -1
  302. package/dist/canvas-react/pixi-react-adapter.js +0 -294
  303. package/dist/canvas-react/world-context.d.ts +0 -98
  304. package/dist/canvas-react/world-context.d.ts.map +0 -1
  305. package/dist/canvas-react/world-context.js +0 -173
  306. package/dist/dev/debug-draw.d.ts +0 -24
  307. package/dist/dev/debug-draw.d.ts.map +0 -1
  308. package/dist/dev/debug-draw.js +0 -73
  309. package/dist/render/auto-batcher.d.ts +0 -34
  310. package/dist/render/auto-batcher.d.ts.map +0 -1
  311. package/dist/render/auto-batcher.js +0 -140
  312. package/dist/render/lod.d.ts +0 -13
  313. package/dist/render/lod.d.ts.map +0 -1
  314. package/dist/render/lod.js +0 -16
  315. package/dist/runtime/types.d.ts +0 -303
  316. package/dist/runtime/types.d.ts.map +0 -1
  317. package/dist/runtime/types.js +0 -1
  318. package/dist/world3d-react/engine-bridge.d.ts +0 -47
  319. package/dist/world3d-react/engine-bridge.d.ts.map +0 -1
  320. package/dist/world3d-react/engine-bridge.js +0 -73
  321. package/dist/world3d-react/r3f-adapter.d.ts +0 -58
  322. package/dist/world3d-react/r3f-adapter.d.ts.map +0 -1
  323. package/dist/world3d-react/r3f-adapter.js +0 -634
  324. package/dist/world3d-react/world-context.d.ts +0 -182
  325. package/dist/world3d-react/world-context.d.ts.map +0 -1
  326. package/dist/world3d-react/world-context.js +0 -235
  327. package/src/adapter/setup-three-root-adapter.ts +0 -1032
  328. package/src/animation/clip-map.ts +0 -34
  329. package/src/asset-registry.ts +0 -89
  330. package/src/canvas-react/engine-bridge.ts +0 -59
  331. package/src/canvas-react/pixi-react-adapter.tsx +0 -356
  332. package/src/canvas-react/world-context.ts +0 -253
  333. package/src/dev/debug-draw.ts +0 -80
  334. package/src/render/auto-batcher.ts +0 -168
  335. package/src/render/lod.ts +0 -17
  336. package/src/runtime/types.ts +0 -328
  337. package/src/world3d-react/engine-bridge.ts +0 -86
  338. package/src/world3d-react/r3f-adapter.tsx +0 -717
  339. package/src/world3d-react/world-context.ts +0 -358
@@ -1,1032 +0,0 @@
1
- /**
2
- * SetupThreeRootAdapter — the first-party implementer of {@link RootAdapter}
3
- * that mounts ONE three root from an imperative `setup(ctx)` function.
4
- *
5
- * This is where Rapier lives now: the host no longer knows about it.
6
- * `mount(host)` builds the full first-party runtime (the body
7
- * that used to live inline in `create-runtime.ts`) from a neutral
8
- * {@link ThreeHostContext}, runs the game's `setup`, and returns a
9
- * {@link MountedThreeRoot}. First-party content is now just one implementer of
10
- * the same interface an unmodified external game implements (Phase B).
11
- *
12
- * It was `VgaiSceneGameAdapter` until WO-9 tasks #6/#9. Both halves of that
13
- * name had stopped being true: it loads no scene (the `.vscn.json` branches
14
- * went with the format in WO-8 — `setup` is the only mount path left), and what
15
- * it mounts is one three ROOT, not a game (a game is the manifest's whole set
16
- * of roots). The R3F/TSX world root is a SEPARATE adapter; this one is
17
- * specifically the `fromSetup` path.
18
- *
19
- * `GameSetupFn` / `GameContext` are imported ONLY here (and in the example
20
- * `fromSetup` wrappers) — never by the host. That confinement is the inversion.
21
- */
22
-
23
- import RAPIER from '@dimforge/rapier3d-compat';
24
- import type { SparkRenderer } from '@sparkjsdev/spark';
25
- import type { EffectComposer } from 'postprocessing';
26
- import * as THREE from 'three';
27
- import { clearAssetCaches } from '../asset-loaders';
28
- import {
29
- createSeededRandom,
30
- DEFAULT_SEEDED_RANDOM_SEED,
31
- getSeededRandom,
32
- } from '../core/seeded-random';
33
- import { createSimClock, getSimClock, type SimClockInternal } from '../core/sim-clock';
34
- import { createSystemRunner } from '../core/system-runner';
35
- import { createDebugDraw } from '../dev/debug-draw';
36
- import { type RenderVitalsRegistration, registerRenderVitals } from '../dev/register-render-vitals';
37
- import {
38
- createRenderDebugAdapter,
39
- frameCaptureContextFor,
40
- type RenderDebugWiring,
41
- } from '../dev/render-debug-adapter';
42
- import { collectRenderMemory } from '../dev/render-memory';
43
- import { RENDER_SUBMIT_PHASE } from '../dev/render-vitals';
44
- import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
45
- import { createWebGLGpuTimer } from '../dev/webgl-gpu-timer';
46
- import { createSceneIndex } from '../ecs/scene-index';
47
- import { getUserData, hasUserData, setUserData } from '../ecs/user-data';
48
- import { InputManager } from '../input/input-manager';
49
- import { setAssetPrefix } from '../loader';
50
- import { createCollisionSystem } from '../physics/collision-system';
51
- import { createPhysicsRegistry } from '../physics/physics-registry';
52
- import { createTransformWriter } from '../physics/transform-writer';
53
- import { updateSceneLODs } from '../render/lod';
54
- import { createSoftParticleDepthPass } from '../render/soft-particle-depth';
55
- import {
56
- disposeSparkRendererWhenIdle,
57
- SPARK_DISCOVERY_INTERVAL_MS,
58
- sceneHasGaussianSplat,
59
- shouldDiscoverGaussianSplat,
60
- } from '../render/spark-renderer-lifecycle';
61
- import { type ViewportShadingMode, ViewportShadingRenderer } from '../render/viewport-shading';
62
- import {
63
- createDebugRegistry,
64
- type DebugRegistry,
65
- getDebugRegistry,
66
- } from '../runtime/debug-registry';
67
- import { devBuildEnabled } from '../runtime/dev-build';
68
- import { disposeDebrisSubtree, type RootFrameHooks } from '../runtime/game';
69
- import type { EditorPreview, GameCleanup, GameContext, GameSetupFn } from '../runtime/types';
70
- import { type AudioContext as GameAudio, setupAudio } from '../setup/setup-audio';
71
- import { setupParticles } from '../setup/setup-particles';
72
- import { setupPhysics, updatePhysicsDebug } from '../setup/setup-physics';
73
- import { createSceneView } from '../setup/setup-renderer';
74
- import { createAudioSystemAdapter, releaseAudioMeters } from './first-party-systems';
75
- import type { ThreeHostContext } from './host-context';
76
- import { createRapierPhysicsAdapter } from './rapier-physics-adapter';
77
- import type { MountedThreeRoot, RootAdapter } from './root-adapter';
78
- import type { SystemAdapters } from './system-adapter';
79
-
80
- /** How a {@link SetupThreeRootAdapter} builds its game (the former RuntimeConfig). */
81
- export interface SetupThreeRootConfig {
82
- /** Game setup function — registers systems and loads scenes. */
83
- setup?: GameSetupFn | undefined;
84
- /** Editor context — passed to setup() when launched from the editor. */
85
- editorPreview?: EditorPreview | undefined;
86
- /** Input map JSON path. Defaults to 'inputmaps/default.inputmap.json'. */
87
- inputMapPath?: string | undefined;
88
- /** URL prefix for relative asset paths. Defaults to '/'. */
89
- assetPrefix?: string | undefined;
90
- /** Stable adapter id (for registry/conformance). Defaults to 'setup-three'. */
91
- id?: string | undefined;
92
- }
93
-
94
- /**
95
- * The first-party `MountedThreeRoot`, with concrete extras the editor host uses for
96
- * first-party features (HMR, physics sync) that are NOT part of the neutral
97
- * interface. The generic host only touches the `MountedThreeRoot` surface.
98
- */
99
- export interface MountedSetupThreeRoot extends MountedThreeRoot {
100
- /**
101
- * First-party brand (checklist item 1 / T7.3 lookahead): a plain `'ctx' in
102
- * mounted` structural check misfires against any foreign mount that happens
103
- * to carry an unrelated `ctx` key. This literal-`true`
104
- * property is what `isFirstPartyMounted` (`runtime/game.ts`) actually
105
- * checks; `game.ts` reads it via a structural `{ firstParty?: unknown }`
106
- * probe so it never needs a value import of this module (the existing
107
- * import is type-only and must stay that way).
108
- */
109
- readonly firstParty: true;
110
- readonly ctx: GameContext;
111
- readonly composer: EffectComposer | null;
112
- /** Render-only developer view; native game materials are restored before the frame returns. */
113
- setViewportShadingMode(mode: ViewportShadingMode): void;
114
- /** Warm-restart: dispose the current game, re-run a new setup on the same ctx. */
115
- hotReload(newSetup: GameSetupFn, editorPreview?: EditorPreview): Promise<void>;
116
- /**
117
- * Phase-partitioned frame entry point (T7.1 slice 2) — wired onto this
118
- * world's `RootInstance` by `registerThreeRoot` (`create-runtime.ts`).
119
- * `Game.runFrame` is the CANONICAL driver going forward: it calls
120
- * `frame.runPhase` once per (phase, substep) for every world, then
121
- * `frame.endFrame` once per substep after all roots finish all phases.
122
- * `update` is the direct entry point to the same `SystemRunner` for hosts
123
- * that do not partition phases themselves.
124
- */
125
- readonly frame: RootFrameHooks;
126
- }
127
-
128
- /** Minimal stand-in for the GPU composer when mounting headlessly. */
129
- function headlessComposer(renderer: THREE.WebGLRenderer): EffectComposer {
130
- return {
131
- render: () => {},
132
- setSize: () => {},
133
- dispose: () => {},
134
- addPass: () => {},
135
- removeAllPasses: () => {},
136
- passes: [],
137
- getRenderer: () => renderer,
138
- } as unknown as EffectComposer;
139
- }
140
-
141
- /**
142
- * Wrap a first-party `setup` function as a {@link RootAdapter}. This is how every
143
- * first-party example becomes an implementer of the same interface an external
144
- * game implements — with no change to the setup's body.
145
- */
146
- export function fromSetup(id: string, setup: GameSetupFn): RootAdapter {
147
- return new SetupThreeRootAdapter({ id, setup });
148
- }
149
-
150
- export class SetupThreeRootAdapter implements RootAdapter {
151
- readonly id: string;
152
- constructor(private readonly config: SetupThreeRootConfig = {}) {
153
- this.id = config.id ?? 'setup-three';
154
- }
155
-
156
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one cohesive first-party bootstrap (moved verbatim from create-runtime); splitting it would obscure the ordering contract
157
- async mount(host: ThreeHostContext): Promise<MountedSetupThreeRoot> {
158
- const {
159
- setup,
160
- editorPreview,
161
- inputMapPath = '/inputmaps/default.inputmap.json',
162
- assetPrefix = '/',
163
- } = this.config;
164
- const headless = host.headless === true;
165
- const { width, height } = host.surface;
166
- const renderer = host.renderer;
167
-
168
- setAssetPrefix(assetPrefix);
169
-
170
- // --- Scene view (scene/camera/composer). Browser: canonical builder.
171
- // Headless: plain scene/camera + a no-op composer (no GPU). ---
172
- let scene: THREE.Scene;
173
- let camera: THREE.PerspectiveCamera;
174
- let composer: EffectComposer;
175
- if (headless) {
176
- scene = new THREE.Scene();
177
- scene.background = new THREE.Color(0x1a1a2e);
178
- camera = new THREE.PerspectiveCamera(60, width / height || 1, 0.1, 1000);
179
- camera.position.set(0, 5, 10);
180
- camera.lookAt(0, 0, 0);
181
- composer = headlessComposer(renderer);
182
- } else {
183
- const view = createSceneView(renderer, width, height);
184
- scene = view.scene;
185
- camera = view.camera;
186
- composer = view.composer;
187
- }
188
-
189
- // --- Rapier physics ---
190
- await RAPIER.init();
191
- const physics = setupPhysics(RAPIER, scene);
192
- const physicsRegistry = createPhysicsRegistry();
193
- const writeTransforms = createTransformWriter(physicsRegistry, scene);
194
-
195
- // --- Input (InputManager attaches window listeners; stub it headlessly) ---
196
- const ownsInput = !host.game;
197
- const input = host.game?.input ?? (headless ? headlessInput() : new InputManager());
198
- if (!headless) {
199
- if (host.game) await host.game.loadInputMap(inputMapPath);
200
- else await input.loadMap(inputMapPath);
201
- }
202
-
203
- // --- Collision system ---
204
- const collisions = createCollisionSystem(physics.rapierWorld, physics.eventQueue);
205
-
206
- // --- Audio (Web Audio — browser only) ---
207
- const audio = headless ? headlessAudio() : setupAudio(camera);
208
-
209
- // --- Particles (three.quarks BatchedRenderer) ---
210
- const particles = setupParticles(scene);
211
- // Scene depth for any soft-particle system registered against this world.
212
- // Inert (not one traversal, not one draw) until `registerParticleSystem`
213
- // arms it — see `../render/soft-particle-depth.ts`.
214
- const softParticleDepth = createSoftParticleDepthPass();
215
-
216
- // --- Debug draw / assets ---
217
- const debugDraw = createDebugDraw(scene);
218
- const assets = host.assets;
219
- // Test/headless adapters may provide the renderer surface needed by the
220
- // runtime without a real WebGL context. GPU timing is optional diagnostics:
221
- // degrade honestly when the renderer/context cannot expose extensions.
222
- const rendererContext =
223
- typeof renderer.getContext === 'function' ? renderer.getContext() : undefined;
224
- const gpuTimer =
225
- !headless && rendererContext && typeof rendererContext.getExtension === 'function'
226
- ? createWebGLGpuTimer(rendererContext)
227
- : null;
228
- // W4b (F11 frame debugger): first-party WebGL2 frame capture, constructed
229
- // under the SAME real-context guard as gpuTimer — a headless/stub mount
230
- // (`frameCaptureContextFor` returns null) never builds it and never
231
- // registers `renderDebug`, so the capability is honestly absent there. The
232
- // capture only patches the context WHILE a `captureFrame()` is armed (see
233
- // beginPass/endPass in the render system below); every non-capture frame
234
- // is zero-overhead.
235
- const frameCaptureContext = frameCaptureContextFor(headless, rendererContext);
236
- const frameCapture = frameCaptureContext ? createWebGLFrameCapture(frameCaptureContext) : null;
237
- const renderDebugWiring: RenderDebugWiring | null = frameCapture
238
- ? createRenderDebugAdapter({
239
- capture: frameCapture,
240
- scene,
241
- memory: () => collectRenderMemory(scene, renderer.info),
242
- })
243
- : null;
244
- const viewportShading = new ViewportShadingRenderer();
245
- let viewportShadingMode: ViewportShadingMode = 'solid';
246
- /** Top-level `renderer.render()` calls the last presentation cost — written
247
- * by the render system, read by `postFrame`. See both for why the count is
248
- * latched rather than differenced at report time. */
249
- let lastPresentationPasses = 0;
250
-
251
- // --- System runner (engine-level systems) ---
252
- const systems = createSystemRunner(host.game?.profiler.systemObserver, 'three');
253
- // D15/T-D15.3/.5: pass the LIVE shared game tick (not a locally-counted
254
- // one) into every `poll()` call — `debugRegistry` is declared further
255
- // down in this same `mount()` (it needs `ctx`/`systemAdapters` to exist
256
- // first), but this callback only ever executes during real frame
257
- // ticks, long after `mount()` has fully returned and `debugRegistry` is
258
- // initialized (a safe closure-over-a-later-const, not a genuine
259
- // use-before-init). `host.game` gates it: a bare mount with no `Game`
260
- // shell behind it (test harnesses predating T7.1) has no shared tick to
261
- // key off, so `InputManager.poll()` falls back to its own internal
262
- // self-incrementing counter instead (unchanged pre-D15 behavior) rather
263
- // than being pinned to a constant `0` forever.
264
- if (ownsInput) systems.add('input', () => input.poll(), { name: 'input.poll' });
265
- systems.add('physics', () => physics.rapierWorld.step(physics.eventQueue), {
266
- name: 'physics.step',
267
- });
268
- systems.add(
269
- 'postPhysics',
270
- () => {
271
- collisions.drain();
272
- writeTransforms();
273
- },
274
- { name: 'physics.sync' },
275
- );
276
- systems.add(
277
- 'preRender',
278
- (dt) => {
279
- particles.batchedRenderer.update(dt);
280
- updateSceneLODs(scene, camera);
281
- sparkDiscoveryElapsedMs += dt * 1_000;
282
- const shouldDiscover = shouldDiscoverGaussianSplat(0, sparkDiscoveryElapsedMs);
283
- if (!sparkRenderer && !sparkRendererLoading && !sparkRendererFailed && shouldDiscover) {
284
- sparkDiscoveryElapsedMs = 0;
285
- if (!sceneHasGaussianSplat(scene)) return;
286
- sparkRendererLoading = true;
287
- void import('@sparkjsdev/spark')
288
- .then(({ SparkRenderer }) => {
289
- if (disposed || sparkRenderer) return;
290
- sparkRenderer = new SparkRenderer({ renderer, enableLod: false });
291
- sparkRenderer.traverse((node) => setUserData(node, 'engineInternal', true));
292
- })
293
- .catch((error: unknown) => {
294
- sparkRendererFailed = true;
295
- // biome-ignore lint/suspicious/noConsole: A missing renderer chunk must fail once and loudly, not retry every frame.
296
- console.error('Failed to install SparkRenderer for a live Gaussian splat.', error);
297
- })
298
- .finally(() => {
299
- sparkRendererLoading = false;
300
- });
301
- }
302
- },
303
- { name: 'materials-and-particles' },
304
- );
305
- if (!headless) {
306
- systems.add(
307
- 'render',
308
- (dt) => {
309
- const profiler = host.game?.profiler;
310
- if (profiler?.enabled) gpuTimer?.begin();
311
- if (sparkRenderer) scene.add(sparkRenderer);
312
- // W4b: arm-gated frame capture wraps the real draw calls. beginPass
313
- // is a no-op unless a captureFrame() armed it; afterRender delivers
314
- // the assembled capture to a pending captureFrame() and restores the
315
- // patched context — in the SAME finally as gpuTimer.end(). Spark is
316
- // added before this so its draws fall inside the captured frame.
317
- // Soft-particle scene depth goes in FRONT of the frame's own draw:
318
- // the fade samples the geometry behind the particle, which cannot be
319
- // the depth attachment the particle is drawn into. Deliberately
320
- // OUTSIDE the capture and profiler brackets below — it is a prepass,
321
- // not part of the frame's presentation, and a captured frame should
322
- // show the draws the user sees.
323
- softParticleDepth.render(renderer, scene, camera);
324
- renderDebugWiring?.beforeRender();
325
- // Issue #1504: CPU render submission, bracketed as its OWN profiler
326
- // phase nested inside the frame's enclosing `render` phase. This is
327
- // the frame's only isolated measurement of "how long did it take to
328
- // hand the draws to the driver" — the `render` phase around it also
329
- // contains physics-debug redraw and every other world's render work,
330
- // and a system span would key the reading to this adapter's system
331
- // NAME. The profiler's phase clock is a stack precisely so this
332
- // bracket cannot truncate its parent (`dev/performance-profiler.ts`).
333
- // It is also what makes a frame a PRESENTATION as far as the vitals
334
- // fold is concerned: no draw, no bracket, no display frame.
335
- profiler?.beginPhase();
336
- // `info.render.frame` counts top-level renderer.render() calls and
337
- // survives `info.reset()`, so its delta across the draw is how many
338
- // passes the composer chain (plus shadows) actually cost.
339
- const passesBefore = renderer.info.render.frame;
340
- try {
341
- viewportShading.render(
342
- scene,
343
- viewportShadingMode,
344
- () => composer.render(dt),
345
- // Scene authoring can temporarily attach editor infrastructure
346
- // (gizmos/helpers on their own layer) to this same native scene.
347
- // Only replace materials the actual game camera can render.
348
- (mesh) => camera.layers.test(mesh.layers),
349
- );
350
- } finally {
351
- profiler?.endPhase(RENDER_SUBMIT_PHASE);
352
- lastPresentationPasses = renderer.info.render.frame - passesBefore;
353
- renderDebugWiring?.afterRender();
354
- sparkRenderer?.removeFromParent();
355
- gpuTimer?.end();
356
- }
357
- if (physics.debugEnabled) updatePhysicsDebug(physics.rapierWorld, physics.debugMesh);
358
- },
359
- { name: 'three.render' },
360
- );
361
- }
362
-
363
- // --- Debug toggle (browser only) ---
364
- const onDebugToggle = (e: KeyboardEvent) => {
365
- if (e.code === 'KeyP') {
366
- physics.debugEnabled = !physics.debugEnabled;
367
- physics.debugMesh.visible = physics.debugEnabled;
368
- }
369
- };
370
- if (!headless) window.addEventListener('keydown', onDebugToggle);
371
-
372
- const postFrame = () => {
373
- if (ownsInput) input.endFrame();
374
- // `renderer.info` is checked, not assumed: a headless/stub renderer
375
- // supplies only what the runtime needs to mount, and a mount with no
376
- // counters must report NOTHING rather than throw once per frame. This
377
- // used to be unreachable because only the editor ever enabled the
378
- // profiler; the engine enables it itself under the dev gate now (see the
379
- // render-vitals seed below), so the assumption has to be paid for.
380
- if (host.game?.profiler.enabled && renderer.info) {
381
- host.game.profiler.reportRender({
382
- gpuMs: gpuTimer?.poll() ?? null,
383
- drawCalls: renderer.info.render.calls,
384
- triangles: renderer.info.render.triangles,
385
- geometries: renderer.info.memory.geometries,
386
- textures: renderer.info.memory.textures,
387
- // The LAST presentation's pass count, not a delta taken here: under
388
- // the display-rate loop this hook runs once per fixed SUBSTEP, so a
389
- // delta measured at this point would read as 0 on every substep that
390
- // did not draw. Same one-frame-warm semantics as the renderer.info
391
- // counters beside it.
392
- renderPasses: lastPresentationPasses,
393
- });
394
- }
395
- };
396
-
397
- // The mounted game's adapter surface (`mounted.systems`). Declared before
398
- // ctx so `ctx.registerSystemAdapter` can close over it: the engine fills
399
- // the first-party entries below; GAME-owned capabilities (networking,
400
- // navigation) are registered by the game's setup through the ctx hook.
401
- // The editor holds this same object reference (setActiveSystems), so
402
- // late registrations are visible to its panels at read time.
403
- const systemAdapters: SystemAdapters = {};
404
-
405
- // P3 — `ctx.clock`. Same "ONE per Game root, shared across every world"
406
- // reach-in as `ctx.debug`/`ctx.random` below: `getSimClock(host.game)`
407
- // returns the clock `createGame` filed, and `runFrameImpl` is what flushes
408
- // it. A bare mount with no Game shell (headless harness, foreign host) gets
409
- // a private, mount-local clock so the field is always a real value — but
410
- // nothing flushes that one, because a mount with no Game has no fixed loop
411
- // to bind timers to and inventing a wall-clock frontier here is exactly the
412
- // hazard `core/sim-clock.ts` exists to avoid. Its `disposeAfter` is
413
- // therefore unreachable; the disposer is still the real one.
414
- const gameClock: SimClockInternal | null = host.game ? getSimClock(host.game) : null;
415
- const clock: SimClockInternal =
416
- gameClock ?? createSimClock({ dispose: (obj) => disposeDebrisSubtree(obj, [ctx]) });
417
- /** Only a mount-local fallback is THIS mount's to dispose — see the
418
- * `clock.dispose` teardown step below for why the game-scoped one is not. */
419
- const ownsClock = gameClock === null;
420
-
421
- // --- GameContext (the first-party runtime surface) ---
422
- const ctx: GameContext = {
423
- scene,
424
- camera,
425
- rapierWorld: physics.rapierWorld,
426
- rapier: RAPIER,
427
- physics: physicsRegistry,
428
- input,
429
- collisions,
430
- composer,
431
- audio,
432
- particles,
433
- debugDraw,
434
- assets,
435
- systems,
436
- clock,
437
- // The live scene index for THIS root (P2 `observe`). Created before
438
- // setup() runs so a game's own objects are indexed as they are added —
439
- // it subscribes to three's `childadded`/`childremoved` on the scene, so
440
- // everything the game builds arrives through it. Disposed in
441
- // disposeGame() below (a warm restart keeps it: same `scene`, same ctx).
442
- sceneIndex: createSceneIndex(scene),
443
- registerSystemAdapter: (kind, adapter) => {
444
- systemAdapters[kind] = adapter;
445
- host.game?.notifySystemAdaptersChanged();
446
- },
447
- };
448
- // Game root (T7.1 slice 1): only present once the host has constructed a
449
- // Game shell (createGameRuntime does this before mount; headless/foreign
450
- // hosts may omit it — ctx.game/ctx.roots simply stay undefined, which is
451
- // the documented zero-break behavior). `ctx.roots` is assigned the SAME
452
- // live array `host.game` mutates via `registerRoot` (not a copy) — the
453
- // default world is registered AFTER mount returns, and this reference
454
- // must observe that later push.
455
- if (host.game) {
456
- ctx.game = host.game;
457
- ctx.roots = host.game.roots;
458
- ctx.playtest = host.game.playtest ?? null;
459
- }
460
- // Everything registered above (the engine systems) is "engine" and must
461
- // survive a warm
462
- // restart. Everything a game's setup() (below) adds
463
- // from here on is "game" content that `hotReload` bulk-removes via
464
- // `systems.removeAllNonEngine()` on every restart (T1.7).
465
- systems.markEngineBoundary();
466
-
467
- // First-party System adapters over the real subsystems — seeded HERE,
468
- // BEFORE setup()/scene load runs below, so that a game which registers any
469
- // of these kinds during its own setup (via `ctx.registerSystemAdapter`)
470
- // always wins over the engine's first-party entry (see
471
- // registerSystemAdapter's docstring in runtime/types.ts). `navigation` is
472
- // ALSO first-party now (R8 item 7): `createNavigationAdapter` wraps the
473
- // resolves, not here (no NavMeshManager exists yet at this point in the
474
- // mount). `networking` remains the one game-owned capability with no
475
- // first-party implementer; it simply starts absent here.
476
- // P-4: the physics seam is keyed by node id, so the first-party mount
477
- // supplies the first-party `id → Object3D` map. `userData.entityId` is
478
- // THIS stack's identity convention (the scene loader stamps it, and the
479
- // editor's `objectMap`/`hierarchy.object3D` agree on it), which is exactly
480
- // why the lookup belongs here and not inside `rapier-physics-adapter.ts`.
481
- // Memoized because the gizmo/inspector ask repeatedly for one selection;
482
- // a miss (or an object since detached) re-walks once and re-caches.
483
- const nodeObjectCache = new Map<string, THREE.Object3D>();
484
- const resolveNodeObject = (nodeId: string): THREE.Object3D | null => {
485
- const cached = nodeObjectCache.get(nodeId);
486
- if (cached?.parent) return cached;
487
- let found: THREE.Object3D | null = null;
488
- scene.traverse((o) => {
489
- if (!found && getUserData(o, 'entityId') === nodeId) found = o;
490
- });
491
- if (found) nodeObjectCache.set(nodeId, found);
492
- else nodeObjectCache.delete(nodeId);
493
- return found;
494
- };
495
- systemAdapters.physics = createRapierPhysicsAdapter(
496
- physicsRegistry,
497
- physics,
498
- resolveNodeObject,
499
- );
500
- // D10/T7.6: the audio seam `Game.play.pause()` silences on pause. Works
501
- // against the headless stand-in too (`headlessAudio()`'s plain
502
- // `masterGain.gain` object) — this world's `pause()` mutes it harmlessly.
503
- systemAdapters.audio = createAudioSystemAdapter(audio);
504
- // Debug/synthetic-player seam (T1.1): ONE registry per Game root, shared
505
- // across every world that mounts onto it — `getDebugRegistry(host.game)`
506
- // reaches the SAME accumulator `createGame` filed, so a second/third
507
- // world's `ctx.debug` feeds it too (see debug-registry.ts's module doc
508
- // for the provenance/id note). A mount with no Game shell at all (bare
509
- // test harnesses predating T7.1) gets a private, mount-local registry
510
- // instead — same absence precedent as `ctx.game`/`ctx.roots` above.
511
- const debugRegistry: DebugRegistry =
512
- (host.game ? getDebugRegistry(host.game) : null) ??
513
- createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
514
- systemAdapters.debug = debugRegistry.adapter;
515
- // W4b: the render-debug capability is engine-owned and first-party, seeded
516
- // here (like physics/input/assets) BEFORE the engineAdapterKinds snapshot
517
- // below so a warm restart / stop never mistakes it for game content. Only
518
- // present when a real WebGL2 context backed the mount (renderDebugWiring).
519
- if (renderDebugWiring) systemAdapters.renderDebug = renderDebugWiring.adapter;
520
- ctx.debug = debugRegistry.forRoot(this.id);
521
- // T1.2: the InputManager is a first-party per-world handle the registry
522
- // has no other way to reach — wired as a lazy supplier (not a direct
523
- // read here), scoped to THIS world's id, so the built-in `input.actions`
524
- // provider always reflects the CURRENT action set, not a snapshot taken
525
- // at mount time. Per-world (not a single last-writer-wins slot) since
526
- // D15/T-D15.5 — see `debug-registry.ts`'s `setInputActionsSource` doc.
527
- debugRegistry.setInputActionsSource(this.id, () =>
528
- typeof input.actionNames === 'function'
529
- ? input.actionNames().map((name) => ({ name, valueType: input.getActionValueType(name) }))
530
- : [],
531
- );
532
- // D15/T-D15.5 — same lazy-supplier pattern (and same headless-stand-in
533
- // guard) as `setInputActionsSource` immediately above: the built-in
534
- // `input.trace` provider always reads the CURRENT recorded trace, never
535
- // a snapshot taken at mount time. `seed`/`fixedDt` are assembled here
536
- // (not inside `InputManager`, which has no business knowing about
537
- // `ctx.random` or the loop) — the two replay-critical metadata fields
538
- // the design doc's format sketch (§2.c) calls for beside the raw
539
- // per-tick deltas; `null` only absent a Game/ctx.random behind this
540
- // mount, matching every other "no Game" fallback in this file.
541
- debugRegistry.setInputTraceSource(this.id, () => {
542
- const raw: { version: 1; ticks: unknown[] } =
543
- typeof input.getInputTrace === 'function'
544
- ? input.getInputTrace()
545
- : { version: 1, ticks: [] };
546
- const seed = (host.game ? getSeededRandom(host.game) : null)?.seed ?? null;
547
- const fixedDt = host.game?.loop.fixedDt ?? null;
548
- return { version: raw.version, seed, fixedDt, ticks: raw.ticks };
549
- });
550
- // Task 2.1 — the same InputManager instance, wired as the debug bridge's
551
- // actuation target (`runtime/debug-bridge.ts`'s `input.*` methods) at the
552
- // same seed spot as `setInputActionsSource` above. Per-world (D15 review
553
- // objection 2 fix) — see `debug-registry.ts`'s `setVirtualInputTarget`
554
- // doc for why this used to be a single last-writer-wins slot.
555
- debugRegistry.setVirtualInputTarget(this.id, {
556
- setVirtualAction: (action, value) => input.setVirtualAction(action, value),
557
- tapVirtualAction: (action) => input.tapVirtualAction(action),
558
- clearVirtualActions: () => input.clearVirtualActions(),
559
- scheduleActionAtTick: (tick, action, value) =>
560
- input.scheduleActionAtTick(tick, action, value),
561
- startInputRecording: () => input.startInputRecording(),
562
- stopInputRecording: () => input.stopInputRecording(),
563
- isInputRecording: () => input.isInputRecording(),
564
- injectAxis: (sourceId, value) => input.injectAxis(sourceId, value),
565
- injectVector2: (sourceId, value) => input.injectVector2(sourceId, value),
566
- injectPointerDelta: (sourceId, delta) => input.injectPointerDelta(sourceId, delta),
567
- injectPointerPosition: (sourceId, value) => input.injectPointerPosition(sourceId, value),
568
- });
569
- // R4-class fix — wire the InputManager's `'input.schedule.dropped'` sink
570
- // to THIS world's debug registry, same seed spot/precedent as
571
- // `setInputActionsSource`/`setVirtualInputTarget` above (typeof-guarded:
572
- // a headless `InputManager` stand-in may have no `setDebugEmit`).
573
- if (!host.game && typeof input.setDebugEmit === 'function') {
574
- input.setDebugEmit((event, detail) => ctx.debug?.emit(event, detail));
575
- }
576
- // D15 (T-D15.1) — same "ONE per Game root, shared across every world"
577
- // pattern as the debug registry immediately above: `getSeededRandom
578
- // (host.game)` reaches the SAME `SeededRandom` `createGame` filed (boot-
579
- // seeded by the manifest-aware mount path, `mount-manifest.ts`, before
580
- // ANY world's `mount()`/`setup()` runs — see `createGame`'s own doc
581
- // comment), so every world's `ctx.random` is the identical instance, not
582
- // a per-world copy. Same absence precedent as `ctx.debug`: a mount with
583
- // no Game shell at all gets a private, mount-local surface instead.
584
- ctx.random =
585
- (host.game ? getSeededRandom(host.game) : null) ??
586
- createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED);
587
-
588
- // --- Live render vitals (issue #1504) -------------------------------------
589
- // Engine-owned and first-party, seeded HERE like `renderDebug` above: a
590
- // running game must be able to explain its own frame cost through the debug
591
- // registry, with no capability to install and nothing for a game to write.
592
- //
593
- // THE GATE, in one place. Three conditions, and the reason for each:
594
- // 1. `devBuildEnabled()` — the ONE owner of "is this a dev/editor
595
- // context" (`runtime/dev-build.ts`). A ship build registers nothing,
596
- // subscribes to nothing, and never enables the profiler.
597
- // 2. a `Game` shell exists — the readings are folded out of that game's
598
- // profiler frames, and a bare mount has no profiler to fold.
599
- // 3. not headless — a headless mount registers no render system at all,
600
- // so it never brackets a submission and never presents a frame. Its
601
- // vitals could only ever read empty, and registering a door that is
602
- // structurally incapable of answering is worse than not having one.
603
- //
604
- // The enabled-gating question, resolved: the profiler is AUTO-ENABLED here
605
- // rather than given a second, cheaper always-on counter tier. A counter
606
- // tier would be a parallel measurement path — exactly the side ledger this
607
- // work exists to avoid — and the profiler already has the one property
608
- // that made the choice: `enabled` is a plain flag whose cost is per-frame
609
- // bookkeeping the editor ALREADY pays (it sets `profiler.enabled = true`
610
- // on play, `CenterDocuments.tsx`), so under the dev gate this is not a new
611
- // cost, it is the same cost arriving a little earlier. Nothing here ever
612
- // turns it OFF: the profiler's other owners (the Performance panel,
613
- // `render-control.ts`'s perfSample) save and restore the prior state, so a
614
- // dev session that enabled it at mount keeps it enabled.
615
- let renderVitals: RenderVitalsRegistration | null = null;
616
- const seedRenderVitals = (): void => {
617
- // `editorPreview ? true : undefined`: an adapter holding an editor
618
- // preview is being authored IN the editor — a dev context by
619
- // definition, even when the editor SPA itself is a production build.
620
- // The packaged runtime bakes `import.meta.env.DEV = false` into its
621
- // prebuilt bundle, which is a fact about how the EDITOR was built, not
622
- // about whether this mount is a dev session — found live: a
623
- // package-native project's vitals never seeded while its capabilities
624
- // and every other project-graph module ran fine. The override argument
625
- // is `devBuildEnabled`'s own documented seam for exactly this
626
- // caller-knows-better case; a standalone/ship mount (no editorPreview)
627
- // still resolves purely from the build env.
628
- if (!host.game || headless || !devBuildEnabled(editorPreview ? true : undefined)) return;
629
- renderVitals?.dispose();
630
- host.game.profiler.enabled = true;
631
- renderVitals = registerRenderVitals({
632
- registry: debugRegistry,
633
- worldId: this.id,
634
- profiler: host.game.profiler,
635
- scene,
636
- renderer,
637
- });
638
- };
639
- seedRenderVitals();
640
-
641
- // Snapshot of the engine-owned adapter kinds, taken right after seeding
642
- // and before any setup() has had a chance to run. `hotReload`/`disposeGame`
643
- // use this to strip every GAME-registered kind (including an override of a
644
- // first-party kind) left over from the outgoing game, so a warm restart or
645
- // a full stop never exposes a disposed game's networking/etc. adapter to
646
- // the next setup.
647
- const engineAdapterKinds = new Set(Object.keys(systemAdapters));
648
- // 'navigation' is engine-owned too (R8 item 7), but the scene load that
649
- // populates it (below, `applySceneNavigation`) happens AFTER this
650
- // snapshot — add it explicitly so the generic "strip every non-engine
651
- // kind" step (hotReload/disposeGame) never mistakes a first-party
652
- // navigation adapter for game-registered content and deletes it. (The
653
- // adapter is still kept in sync with the CURRENT scene's NavMeshManager
654
- // via the dedicated `clear navigation adapter` teardown step below, which
655
- // runs independently of this snapshot.)
656
- engineAdapterKinds.add('navigation');
657
-
658
- // --- Scene / game setup ---
659
- const engineSceneChildren = new Set(scene.children.slice());
660
- // Tag engine-owned infra (particle BatchedRenderer, debug-draw + subtrees) so
661
- // the editor's play-mode hierarchy skips them — they're not game entities.
662
- for (const c of engineSceneChildren) {
663
- c.traverse((n) => setUserData(n, 'engineInternal', true));
664
- }
665
- let currentCleanup: GameCleanup | null = null;
666
- let sparkRenderer: SparkRenderer | null = null;
667
- let sparkRendererLoading = false;
668
- let sparkRendererFailed = false;
669
- // Discovery is render-loop scheduling, so advance it from the loop's `dt`
670
- // instead of introducing a second wall-clock frontier into engine code.
671
- // Start at the threshold so the first rendered frame discovers eager splats.
672
- let sparkDiscoveryElapsedMs = SPARK_DISCOVERY_INTERVAL_MS;
673
-
674
- if (!setup) {
675
- throw new Error(
676
- 'SetupThreeRootAdapter: no `setup` supplied. The `.vscn.json` scene branches ' +
677
- '(`scenePath` / `sceneData`) were removed with the format; a first-party three root ' +
678
- 'is either an imperative ' +
679
- '`setup(ctx)` (wrap it with `fromSetup(id, setup)`) or a TSX/R3F world root ' +
680
- '(`entry` pointing at a .tsx module — mounted by the R3F adapter, not this one).',
681
- );
682
- }
683
- // Headless is legal here: `createSceneView` was skipped above, the composer
684
- // is a no-op stand-in, and `setup` gets a real `THREE.Scene`/camera. It used
685
- // to throw because headless existed only for the `.vscn` `sceneData` branch;
686
- // with the format gone (WO-8) `setup` is the only mount path, and headless
687
- // unit mounts are what exercise the Game/roots/frame machinery without WebGL.
688
- currentCleanup = await setup(ctx, editorPreview);
689
-
690
- // Warm the GPU program cache before the first visible frame. A mount gets
691
- // a fresh WebGL context, so without this every scene material compiles
692
- // lazily INSIDE the first draw calls — measured as ~0.5s of main-thread
693
- // driver stalls on every editor play press. Gated on
694
- // KHR_parallel_shader_compile: with the extension, compileAsync overlaps
695
- // the compiles with the play transition; WITHOUT it (software
696
- // rasterizers — SwiftShader in CI) compileAsync degenerates into one big
697
- // synchronous compile that delays mount by whole seconds, strictly worse
698
- // than the lazy first-draw path it was replacing. Best-effort: lazy
699
- // compilation stays correct if it throws, and test hosts pass renderer
700
- // stubs without these methods.
701
- const glContext = typeof renderer.getContext === 'function' ? renderer.getContext() : undefined;
702
- const parallelCompile =
703
- !!glContext &&
704
- typeof glContext.getExtension === 'function' &&
705
- !!glContext.getExtension('KHR_parallel_shader_compile');
706
- if (!headless && parallelCompile && typeof renderer.compileAsync === 'function') {
707
- try {
708
- await renderer.compileAsync(scene, camera);
709
- } catch {
710
- // Fall back to lazy compilation.
711
- }
712
- }
713
-
714
- // Idempotency guard for disposeGame — see below. Declared here (not inside
715
- // disposeGame) so hotReload can also refuse to run on an already-disposed
716
- // adapter.
717
- let disposed = false;
718
-
719
- // Run one teardown step in isolation: a throwing step (e.g. a game's own
720
- // cleanup misbehaving, or a WASM free() panicking) must
721
- // not abort the remaining steps — otherwise a single bad step leaks
722
- // the Rapier world and skips input.dispose() with no way to retry (a
723
- // second call is a no-op once `disposed` is set). Logs and continues.
724
- const safeStep = (label: string, fn: () => void): void => {
725
- try {
726
- fn();
727
- } catch (err) {
728
- console.error(`SetupThreeRootAdapter: teardown step "${label}" threw (continuing)`, err);
729
- }
730
- };
731
-
732
- const disposeGame = (): void => {
733
- // A second stop() must be a safe no-op — do NOT double-free the Rapier
734
- // world/EventQueue (WASM handles panic/throw on a second free) or
735
- // double-run teardown side effects.
736
- if (disposed) return;
737
- disposed = true;
738
-
739
- safeStep('currentCleanup.dispose', () => {
740
- currentCleanup?.dispose();
741
- currentCleanup = null;
742
- });
743
- safeStep('sparkRenderer.dispose', () => {
744
- if (!sparkRenderer) return;
745
- sparkRenderer.removeFromParent();
746
- disposeSparkRendererWhenIdle(sparkRenderer);
747
- sparkRenderer = null;
748
- });
749
- // The scene instance's NavMeshManager (if any) was just disposed above
750
- // (currentCleanup.dispose calls `inst.navMesh?.dispose(scene)`) — clear
751
- // the wrapper too, or `mounted.systems.navigation` would keep pointing
752
- // at a freed manager (R8 item 7). Unconditional: harmless if navigation
753
- // was never set.
754
- safeStep('clear navigation adapter', () => {
755
- delete systemAdapters.navigation;
756
- });
757
- if (!headless) {
758
- safeStep('removeEventListener(keydown)', () =>
759
- window.removeEventListener('keydown', onDebugToggle),
760
- );
761
- }
762
-
763
- // P3 — the MOUNT-LOCAL clock only, and only when this mount created one
764
- // (a bare mount with no Game shell). The game-scoped clock is disposed by
765
- // `GameInternal.dispose()`, which `create-runtime.ts` runs after every
766
- // root has torn down — because disposing ONE world's mount is a supported
767
- // way to end a sub-session while the Game keeps running, and destroying a
768
- // shared clock there froze `now()` and killed the sibling worlds' timers.
769
- // The asymmetry is the tell: `seededRandom` and `debugRegistry` are
770
- // game-scoped too, and this teardown destroys neither (it strips only its
771
- // OWN slice of the registry, a few steps below).
772
- //
773
- // Sim timers still go BEFORE the Rapier world they would touch: a pending
774
- // `disposeAfter`/`after` fires into a live Rapier world, and once that is
775
- // gone it would operate on freed WASM handles.
776
- // Disposing cancels every timer and rejects every pending `delay` with an
777
- // `AbortError` (which awaiting game code is documented to tolerate,
778
- // exactly like an aborted `fetch`).
779
- if (ownsClock) safeStep('clock.dispose', () => clock.dispose());
780
-
781
- // Before the scene is emptied below: dropping the listeners
782
- // first means the teardown's ~N `scene.remove()` calls do no index
783
- // bookkeeping, and — the failure mode this exists to prevent — no
784
- // `childadded` listener is left behind on a disposed scene.
785
- safeStep('sceneIndex.dispose', () => ctx.sceneIndex.dispose());
786
-
787
- safeStep('remove rigid bodies', () => {
788
- const handles: number[] = [];
789
- physics.rapierWorld.forEachRigidBody((b) => handles.push(b.handle));
790
- for (const h of handles) {
791
- const b = physics.rapierWorld.getRigidBody(h);
792
- if (b) physics.rapierWorld.removeRigidBody(b);
793
- }
794
- });
795
- safeStep('rapierWorld.free', () => physics.rapierWorld.free());
796
- safeStep('eventQueue.free', () => physics.eventQueue.free()); // WASM handle — must be freed manually
797
- safeStep('physicsRegistry.clear', () => physicsRegistry.clear());
798
-
799
- safeStep('scene children dispose', () => {
800
- for (const obj of [...scene.children]) {
801
- scene.remove(obj);
802
- obj.traverse((node) => {
803
- if (node instanceof THREE.Mesh) {
804
- if (!hasUserData(node, '__sharedGeometry')) node.geometry?.dispose();
805
- if (Array.isArray(node.material))
806
- node.material.forEach((m) => {
807
- m.dispose();
808
- });
809
- else node.material?.dispose();
810
- }
811
- });
812
- }
813
- scene.fog = null;
814
- scene.background = null;
815
- scene.environment = null;
816
- });
817
- safeStep('clearAssetCaches', () => clearAssetCaches());
818
- safeStep('debugDraw.clear', () => debugDraw.clear());
819
-
820
- // R8 item 7 (C) — disposeGame (Stop) used to skip both of these, unlike
821
- // hotReload (warm restart), which already ran them: a game lifecycle
822
- // system registered via `ctx.systems.register(...)` never had its
823
- // `dispose()` hook called on a full Stop, and a game-registered
824
- // systemAdapters kind (e.g. a `setup`-registered 'networking') survived
825
- // a Stop and would be visible on a STALE `mounted.systems` even though
826
- // the game that owned it is gone. Same two steps, same relative
827
- // ordering as hotReload (before the
828
- // engine-owned audio/input teardown below, which hotReload doesn't
829
- // touch at all since it warm-restarts rather than tearing engine
830
- // subsystems down).
831
- safeStep('systems.removeAllNonEngine', () => systems.removeAllNonEngine());
832
- safeStep('strip game-registered systemAdapters', () => {
833
- for (const kind of Object.keys(systemAdapters)) {
834
- if (!engineAdapterKinds.has(kind)) delete systemAdapters[kind as keyof SystemAdapters];
835
- }
836
- });
837
- // A game-hosted mount owns only its world-scoped registrations; sibling
838
- // roots remain alive during root replacement. A bare standalone adapter
839
- // owns the registry and can clear it globally.
840
- safeStep('debugRegistry.strip', () =>
841
- host.game ? debugRegistry.strip(this.id) : debugRegistry.strip(),
842
- );
843
- // The strip above removes the vitals REGISTRATIONS (their provenance is
844
- // this world). This ends the fold's profiler subscription — the one
845
- // resource `registerRenderVitals` allocates that a strip cannot reach.
846
- safeStep('renderVitals.dispose', () => {
847
- renderVitals?.dispose();
848
- renderVitals = null;
849
- });
850
-
851
- // This mount created the depth pass, so this mount ends it — the render
852
- // target it holds is drawing-buffer sized and nothing else can reach it.
853
- safeStep('softParticleDepth.dispose', () => softParticleDepth.dispose());
854
-
855
- // Release analyser taps BEFORE the audio teardown below. A consumer
856
- // (the editor's meter poll) should dispose its own handle, but Stop must
857
- // never leak taps regardless.
858
- //
859
- // This used to ride on the Tone introspection module's disposer, which
860
- // meant a game that never touched Tone leaked them — the module was
861
- // never loaded, so nothing ever called this. `acquireMeters` belongs to
862
- // the base adapter (535d248dc), so its release does too, and the world
863
- // that created the adapter is what disposes it.
864
- safeStep('audio meters release', () => {
865
- const audioAdapter = systemAdapters.audio;
866
- if (audioAdapter) releaseAudioMeters(audioAdapter);
867
- });
868
-
869
- if (!headless) {
870
- safeStep('audio teardown', () => {
871
- audio.masterGain.gain.value = 0;
872
- audio.listener.context.suspend();
873
- camera.remove(audio.listener);
874
- composer.dispose();
875
- });
876
- }
877
- if (ownsInput) safeStep('input.dispose', () => input.dispose());
878
- // W4b: restore any pending capture wrappers and reject a capture in
879
- // flight (a Stop mid-capture must not leave the context patched).
880
- safeStep('renderDebug.dispose', () => renderDebugWiring?.dispose());
881
- safeStep('gpuTimer.dispose', () => gpuTimer?.dispose());
882
- safeStep('viewportShading.dispose', () => viewportShading.dispose());
883
- };
884
-
885
- const hotReload = async (
886
- newSetup: GameSetupFn,
887
- newEditorPreview?: EditorPreview,
888
- ): Promise<void> => {
889
- if (disposed) {
890
- throw new Error('SetupThreeRootAdapter: cannot hotReload after dispose()/stop()');
891
- }
892
-
893
- // Full parity with disposeGame for the parts a warm restart rebuilds:
894
- // game cleanup, component batch teardown (in that
895
- // ordering, BEFORE bodies are pulled from the world — see disposeGame),
896
- // scene-UI dispose (React root unmount), and bulk system removal. Each
897
- // step isolated the same way as disposeGame — one throwing step (e.g. a
898
- // component dispose) must not abort the rest of the warm restart.
899
- safeStep('currentCleanup.dispose', () => {
900
- currentCleanup?.dispose();
901
- currentCleanup = null;
902
- });
903
- // Clear any navigation adapter a previous `setup` registered: `hotReload`'s
904
- // `newSetup` (below) never auto-wires a replacement, so leaving the old
905
- // wrapper would point at a disposed manager. A `newSetup` that wants navigation may
906
- // register its own via `ctx.registerSystemAdapter('navigation', ...)`.
907
- safeStep('clear navigation adapter', () => {
908
- delete systemAdapters.navigation;
909
- });
910
-
911
- safeStep('remove rigid bodies', () => {
912
- const handles: number[] = [];
913
- physics.rapierWorld.forEachRigidBody((b) => handles.push(b.handle));
914
- for (const h of handles) {
915
- const b = physics.rapierWorld.getRigidBody(h);
916
- if (b) physics.rapierWorld.removeRigidBody(b);
917
- }
918
- physicsRegistry.clear();
919
- });
920
- safeStep('scene children dispose', () => {
921
- for (const obj of [...scene.children]) {
922
- if (engineSceneChildren.has(obj)) continue;
923
- scene.remove(obj);
924
- obj.traverse((node) => {
925
- if (node instanceof THREE.Mesh) {
926
- if (!hasUserData(node, '__sharedGeometry')) node.geometry?.dispose();
927
- if (Array.isArray(node.material))
928
- node.material.forEach((m) => {
929
- m.dispose();
930
- });
931
- else node.material?.dispose();
932
- }
933
- });
934
- }
935
- scene.fog = null;
936
- scene.background = null;
937
- scene.environment = null;
938
- });
939
- safeStep('debugDraw.clear', () => debugDraw.clear());
940
-
941
- // Bulk-remove every system the outgoing game registered (this restart's
942
- // predecessor) so warm restarts never accumulate duplicate systems —
943
- // engine systems registered before mount()'s boundary mark are untouched.
944
- safeStep('systems.removeAllNonEngine', () => systems.removeAllNonEngine());
945
-
946
- // Strip every GAME-registered systemAdapters kind left by the outgoing
947
- // game (including any override of a first-party kind) so the disposed
948
- // game's networking/etc. adapter is never exposed to the next setup —
949
- // see registerSystemAdapter's docstring in runtime/types.ts. ('navigation'
950
- // is engine-kind and is NOT touched here — it's a first-party adapter now,
951
- // kept in sync with the current scene's NavMeshManager by the dedicated
952
- // `clear navigation adapter` step above instead.) The new setup below
953
- // either re-registers a kind or it stays absent.
954
- safeStep('strip game-registered systemAdapters', () => {
955
- for (const kind of Object.keys(systemAdapters)) {
956
- if (!engineAdapterKinds.has(kind)) delete systemAdapters[kind as keyof SystemAdapters];
957
- }
958
- });
959
- // Hot-reload re-seed (spec §3.1): the incoming game's setup (below)
960
- // re-registers its providers/commands under the SAME worldId — strip()
961
- // clears the outgoing game's registrations first so that re-seed is
962
- // silent (no stale duplicate-name warning) rather than "replacing" a
963
- // dead game's entries. Defect 2 fix: scoped to THIS mount's worldId
964
- // (`this.id`) — a blind global strip() here wiped every OTHER live
965
- // world's registrations on every warm restart of just this one, plus
966
- // every react-door registration (whose `useEffect` cleanup never
967
- // re-fires to restore them after someone else's strip).
968
- safeStep('debugRegistry.strip', () => debugRegistry.strip(this.id));
969
- // The strip above takes the ENGINE's own vitals registrations with it
970
- // (they carry this world's provenance, which is what makes the game's
971
- // own re-seed silent). Re-seed them before the incoming setup runs, so a
972
- // warm restart does not silently cost a game its render vitals — and so
973
- // the fold starts clean rather than carrying the outgoing game's frames.
974
- safeStep('renderVitals re-seed', seedRenderVitals);
975
-
976
- currentCleanup = await newSetup(ctx, newEditorPreview);
977
- };
978
-
979
- return {
980
- kind: 'three',
981
- scene,
982
- camera,
983
- firstParty: true,
984
- drivesOwnLoop: false,
985
- update: (dt: number) => {
986
- systems.run(dt);
987
- postFrame();
988
- },
989
- resize: (w: number, h: number) => {
990
- const safeWidth = Math.max(1, w);
991
- const safeHeight = Math.max(1, h);
992
- composer.setSize(safeWidth, safeHeight);
993
- camera.aspect = safeWidth / safeHeight;
994
- camera.updateProjectionMatrix();
995
- },
996
- dispose: disposeGame,
997
- systems: systemAdapters,
998
- ctx,
999
- composer: headless ? null : composer,
1000
- setViewportShadingMode: (mode) => {
1001
- viewportShadingMode = mode;
1002
- },
1003
- hotReload,
1004
- frame: {
1005
- runPhase: (phase, dt) => systems.runPhase(phase, dt),
1006
- endFrame: postFrame,
1007
- },
1008
- };
1009
- }
1010
- }
1011
-
1012
- /** Headless input stand-in — `InputManager` attaches `window` listeners in its
1013
- * constructor, which Node lacks. A headless mount never reads input. */
1014
- function headlessInput(): InputManager {
1015
- return {
1016
- poll: () => {},
1017
- endFrame: () => {},
1018
- dispose: () => {},
1019
- loadMap: async () => {},
1020
- isPressed: () => false,
1021
- getAxis: () => 0,
1022
- } as unknown as InputManager;
1023
- }
1024
-
1025
- /** Headless audio stand-in (no Web Audio). Only the fields the mount/cleanup
1026
- * touch are present. */
1027
- function headlessAudio(): GameAudio {
1028
- return {
1029
- listener: undefined as unknown as THREE.AudioListener,
1030
- masterGain: { gain: { value: 1 } },
1031
- } as unknown as GameAudio;
1032
- }