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