@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
@@ -4,11 +4,9 @@
4
4
  *
5
5
  * WHY IT EXISTS. `SystemAdapters.physics` is what the editor's transform
6
6
  * coordination (`freeze → apply → unfreeze`) and the universal
7
- * "Colliders drawn" instrument (`@engine/dev/instruments`) read. The
8
- * first-party imperative three root registers one from
9
- * `../adapter/setup-three-root-adapter.ts`; an R3F world builds no first-party
10
- * Rapier runtime at all (see `R3FGameContext`'s doc comment) — it does physics
11
- * with `@react-three/rapier` INSIDE the fiber tree, where nothing engine-side
7
+ * "Colliders drawn" instrument (`@engine/dev/instruments`) read. An R3F world
8
+ * builds no first-party Rapier runtime at all it does physics with
9
+ * `@react-three/rapier` INSIDE the fiber tree, where nothing engine-side
12
10
  * can see it. Result, before this: the colliders instrument read `null` (an
13
11
  * em-dash) and its toggle threw "no physics adapter" in every shipped example
14
12
  * and every scaffolded game.
@@ -40,18 +38,23 @@
40
38
  * is how a `SystemAdapters` member says "not supported".
41
39
  *
42
40
  * ── RESOURCE OWNERSHIP, STATED ONCE ─────────────────────────────────────────
43
- * OWNER: this component owns exactly ONE `SystemAdapters.physics` registration,
44
- * on the R3F root it is mounted in. It allocates nothing else no timers, no
45
- * subscriptions, no per-frame work (see the phase note below), and no Rapier
46
- * objects: the bodies it edits belong to `<Physics>`.
47
- * SHARERS: every reader of `game.systemAdapters.physics` the
48
- * colliders instrument, the editor's transform coordination. They read; they
49
- * never dispose.
50
- * TEARDOWN: the effect's cleanup, and nothing else — it clears the SAME key it
51
- * set (`registerSystemAdapter('physics', undefined)`, which
52
- * `Game.computeSystemAdapters` skips), so a play stop/start, a hot restart, or
53
- * a world that unmounts `<Physics>` leaves no stale adapter behind. Two
54
- * bridges in one root would fight over that one key; mount exactly one.
41
+ * OWNER: this component owns one physics ADAPTER, published onto the ONE
42
+ * module-scoped slot `rapierPhysicsSystem()` forwards to (GAME-GLOBALthe
43
+ * entry's static `systems.physics` declaration cannot be per-root). It allocates nothing
44
+ * else no timers, no subscriptions, no per-frame work (see the phase note
45
+ * below), and no Rapier objects: the bodies it edits belong to `<Physics>`.
46
+ * SHARERS: every reader of `game.systemAdapters.physics` the colliders
47
+ * instrument, the editor's transform coordination. They read; they never
48
+ * dispose.
49
+ * TEARDOWN: the effect's cleanup, and nothing else — it clears the SAME key
50
+ * and the slot it set (identity-guarded), so a play stop/start, a hot
51
+ * restart, or a world that unmounts `<Physics>` leaves no stale adapter
52
+ * behind. Because the slot is PAGE-global (see its doc below an engine
53
+ * module is shared across the editor's Play instances), mount exactly ONE
54
+ * bridge per PAGE: with two — a scene that mounts two, or two Play instances
55
+ * of one rapier world — the later mount wins the slot and the later unmount
56
+ * clears it while the earlier bridge is still live. That state is reported
57
+ * loudly the moment it starts (`warnOnContendedMount`).
55
58
  *
56
59
  * PHASE ORDERING: none to respect — this component registers pull-only
57
60
  * callbacks and runs no per-frame work. Every method it exposes is invoked
@@ -62,7 +65,7 @@ import { useRapier } from '@react-three/rapier';
62
65
  import { useEffect } from 'react';
63
66
  import * as THREE from 'three';
64
67
  import { createRapierBodyEditing, } from '../adapter/rapier-physics-adapter';
65
- import { useGameContext } from './engine-bridge';
68
+ import { createSystemSlot } from '../adapter/system-slot';
66
69
  function lateEntityObject(states, nodeId) {
67
70
  let found;
68
71
  for (const state of states) {
@@ -92,50 +95,51 @@ function owningBody(object, states) {
92
95
  * `useRapier()`). Renders nothing.
93
96
  */
94
97
  export function RapierPhysicsBridge({ onDebugChange }) {
95
- const ctx = useGameContext();
98
+ // This bridge publishes the module slot below, which the entry's
99
+ // `systems.physics` declaration (`rapierPhysicsSystem()`) forwards to —
100
+ // that declaration is the ONE path the host reads this world's physics
101
+ // through.
96
102
  const { colliderStates, rapier, rigidBodyStates, world } = useRapier();
97
- const registerSystemAdapter = ctx.registerSystemAdapter;
98
103
  useEffect(() => {
99
- if (!registerSystemAdapter)
100
- return;
101
104
  /**
102
- * node id → the Rapier body driving it, in two hops the library and the
103
- * engine each already own:
105
+ * node id → the Rapier body driving it, entirely through the library's
106
+ * OWN body↔object map (`rigidBodyStates`, carried on the `RapierContext`
107
+ * `useRapier()` returns): resolve `userData.entityId` — this stack's
108
+ * identity convention, stamped by the editor's R3F authoring adapter —
109
+ * inside the body-owned subtrees, then walk ANCESTORS to the owning body
110
+ * (`<RigidBody>` owns a subtree, so the selected mesh is usually a
111
+ * descendant of the object the state names).
104
112
  *
105
- * 1. id `Object3D`, through the live scene index `userData.entityId`
106
- * is this stack's identity convention (the editor's R3F authoring
107
- * adapter stamps it; `../adapter/setup-three-root-adapter.ts` resolves
108
- * the same way for the imperative root).
109
- * 2. `Object3D` → body, through `rigidBodyStates` — the library's OWN
110
- * body↔object map, carried on the `RapierContext` `useRapier()`
111
- * returns. Walking ANCESTORS matters: `<RigidBody>` owns a subtree, so
112
- * the selected mesh is usually a descendant of the object the state
113
- * names.
114
- *
115
- * Both maps are live, so this is computed per call rather than cached —
113
+ * The map is live, so this is computed per call rather than cached
116
114
  * calls arrive at gesture rate (a gizmo drag start/end, a dev command),
117
- * never per frame. `ctx.sceneIndex` is read AT CALL TIME, never captured:
118
- * the R3F root backfills it once fiber resolves the scene, which can land
119
- * after this component's first effect (`world-context.ts`'s
120
- * `setSceneCamera`), so a destructured copy would be permanently absent.
115
+ * never per frame, and only objects under a `<RigidBody>` can have a body
116
+ * at all, so the body subtrees are the complete search space.
121
117
  */
122
118
  const bodyFor = (nodeId) => {
123
- const indexed = ctx.sceneIndex?.byEntityId(nodeId);
124
- const object = indexed ?? lateEntityObject(rigidBodyStates.values(), nodeId);
125
- if (!indexed && object) {
126
- // The editor assigns its stable authoring entity ids AFTER Fiber's
127
- // first commit, while the root's SceneIndex is constructed during
128
- // that commit. Resolve the one late id inside the library-owned body
129
- // subtrees and immediately reindex it; subsequent gestures take the
130
- // normal indexed path. This is the SceneIndex's documented escape
131
- // hatch for a userData id written after construction, not a parallel
132
- // scene mirror.
133
- ctx.sceneIndex?.reindex(object);
134
- }
119
+ const object = lateEntityObject(rigidBodyStates.values(), nodeId);
135
120
  if (!object)
136
121
  return undefined;
137
122
  return owningBody(object, rigidBodyStates.values());
138
123
  };
124
+ /**
125
+ * The same question, with the two negative answers KEPT APART for the
126
+ * editing protocol — see {@link RapierBodyLookup}. This world's search
127
+ * space is the `<RigidBody>` subtrees, so "no object here" really is "not a
128
+ * node of mine", not "a node of mine without physics".
129
+ */
130
+ const bodyLookup = (nodeId) => {
131
+ const object = lateEntityObject(rigidBodyStates.values(), nodeId);
132
+ if (!object)
133
+ return { kind: 'unresolved' };
134
+ const body = owningBody(object, rigidBodyStates.values());
135
+ return body ? { kind: 'body', body } : { kind: 'no-body' };
136
+ };
137
+ /** Rapier's own name for a shape type, via the enum's reverse mapping —
138
+ * so an unprojected collider reports WHICH shape it is (`TriMesh`,
139
+ * `HeightField`, …) instead of a bare number, and a shape added by a
140
+ * future rapier still names itself. */
141
+ const rapierShapeName = (shapeType) => rapier.ShapeType[shapeType] ??
142
+ `shapeType ${shapeType}`;
139
143
  const colliderShape = (collider) => {
140
144
  switch (collider.shapeType()) {
141
145
  case rapier.ShapeType.Cuboid: {
@@ -150,8 +154,13 @@ export function RapierPhysicsBridge({ onDebugChange }) {
150
154
  halfHeight: collider.halfHeight(),
151
155
  radius: collider.radius(),
152
156
  };
157
+ // Rapier has many more shapes than this seam projects (TriMesh,
158
+ // HeightField, ConvexPolyhedron, Cylinder, Cone, …). The collider is
159
+ // REAL; only our plain-data projection of it is missing, so it is
160
+ // reported as itself rather than dropped — a body whose only collider
161
+ // is a trimesh used to inspect as a body with no colliders at all.
153
162
  default:
154
- return null;
163
+ return { type: 'unsupported', kind: rapierShapeName(collider.shapeType()) };
155
164
  }
156
165
  };
157
166
  const colliders = (nodeId) => {
@@ -163,8 +172,6 @@ export function RapierPhysicsBridge({ onDebugChange }) {
163
172
  if (state.collider.parent()?.handle !== body.handle)
164
173
  continue;
165
174
  const shape = colliderShape(state.collider);
166
- if (!shape)
167
- continue;
168
175
  state.object.updateWorldMatrix(true, false);
169
176
  const position = state.collider.translation();
170
177
  const rotation = state.collider.rotation();
@@ -199,6 +206,11 @@ export function RapierPhysicsBridge({ onDebugChange }) {
199
206
  collider.setHalfHeight(shape.halfHeight);
200
207
  collider.setRadius(shape.radius);
201
208
  break;
209
+ // A shape this seam does not project has no dimensions to write back.
210
+ // `colliders()` reports it so the body's physics is not invisible; that
211
+ // is a READ, and it does not make the shape editable.
212
+ case 'unsupported':
213
+ break;
202
214
  }
203
215
  };
204
216
  const jointType = (type) => {
@@ -296,15 +308,88 @@ export function RapierPhysicsBridge({ onDebugChange }) {
296
308
  joint.setAnchor2(value);
297
309
  };
298
310
  const adapter = {
299
- ...createRapierBodyEditing(bodyFor),
311
+ ...createRapierBodyEditing(bodyLookup),
300
312
  setDebugDrawEnabled: (enabled) => onDebugChange(enabled),
301
313
  colliders,
302
314
  previewCollider,
303
315
  joints,
304
316
  previewJointAnchor,
305
317
  };
306
- registerSystemAdapter('physics', adapter);
307
- return () => registerSystemAdapter('physics', undefined);
308
- }, [colliderStates, ctx, onDebugChange, rapier, registerSystemAdapter, rigidBodyStates, world]);
318
+ warnOnContendedMount();
319
+ const detach = physicsSlot.attach(adapter);
320
+ return () => {
321
+ liveBridgeCount -= 1;
322
+ detach();
323
+ };
324
+ }, [colliderStates, onDebugChange, rapier, rigidBodyStates, world]);
309
325
  return null;
310
326
  }
327
+ // ---------------------------------------------------------------------------
328
+ // The entry-declaration half
329
+ // ---------------------------------------------------------------------------
330
+ /**
331
+ * The bridge's slot. Module-scoped on purpose: the entry's `systems`
332
+ * declaration is static, the rapier world is not, and this slot is the seam
333
+ * between the two. Module scope here means one per PAGE, not one per game:
334
+ * this file is an ENGINE module, reached by bare specifier, so the editor's
335
+ * multi-instance Play (which stamps mount ids onto PROJECT-owned specifiers
336
+ * only — `server/project-module-instance.ts`) shares this one slot across
337
+ * every mounted instance. Two live instances of a rapier world therefore
338
+ * contend for it: the LAST bridge to mount wins every instance's declared
339
+ * `systems.physics`, and the loser's detach is dropped by the identity guard.
340
+ * {@link warnOnContendedMount} makes that state loud at the moment it
341
+ * happens; making the slot instance-scoped needs a per-mount rendezvous the
342
+ * context-free declaration (`rapierPhysicsSystem()` at project-module scope)
343
+ * does not yet have.
344
+ *
345
+ * Every member is an ACTION: each one either edits the sim or reads a live
346
+ * Rapier handle, and all of them arrive at gesture rate (a gizmo drag, an
347
+ * instrument command), never from a per-frame loop — so refusing by name is
348
+ * always the right answer, and a fabricated empty never is.
349
+ * `debugDraw`/`contactPoints` are OMITTED, not stubbed, for the anti-shim
350
+ * reason stated at the top of this file; a member the config does not list
351
+ * does not exist on the slot.
352
+ */
353
+ let liveBridgeCount = 0;
354
+ /** The page-scope contention above, made LOUD instead of silent (never a
355
+ * silent degrade): a second concurrent bridge mount — a scene that mounted
356
+ * two, or a second Play instance of a rapier world — is reported the moment
357
+ * it happens, naming what breaks and until when. */
358
+ function warnOnContendedMount() {
359
+ liveBridgeCount += 1;
360
+ if (liveBridgeCount <= 1)
361
+ return;
362
+ // biome-ignore lint/suspicious/noConsole: the standing-warning channel for a capability the adapter cannot reach — the editor console surfaces it during play
363
+ console.warn(`[rapierPhysicsSystem] ${liveBridgeCount} <RapierPhysicsBridge> mounts are live on this ` +
364
+ 'page, but the declared physics slot is one per page (engine modules are shared across ' +
365
+ "Play instances). Every instance's `systems.physics` now forwards to the LAST bridge " +
366
+ 'mounted, so physics gizmo edits and instruments address that world — not necessarily ' +
367
+ 'the one selected — until the extra mounts unmount.');
368
+ }
369
+ const physicsSlot = createSystemSlot({
370
+ name: 'rapierPhysicsSystem',
371
+ unattachedMessage: 'no <RapierPhysicsBridge> is mounted inside a live <Physics> right now — the world is not ' +
372
+ 'playing, or the scene never mounted the bridge.',
373
+ members: {
374
+ ownerOf: { kind: 'action' },
375
+ freeze: { kind: 'action' },
376
+ commit: { kind: 'action' },
377
+ unfreeze: { kind: 'action' },
378
+ setDebugDrawEnabled: { kind: 'action' },
379
+ colliders: { kind: 'action' },
380
+ previewCollider: { kind: 'action' },
381
+ joints: { kind: 'action' },
382
+ previewJointAnchor: { kind: 'action' },
383
+ },
384
+ });
385
+ /**
386
+ * The `systems.physics` slot for a default-exported world — declare it from
387
+ * the entry (`export const systems = { physics: rapierPhysicsSystem() }`) and
388
+ * keep `<RapierPhysicsBridge />` mounted inside `<Physics>`. Every call
389
+ * forwards to the LIVE bridge adapter, so the static declaration answers with
390
+ * the running world's physics; while none is mounted, every call refuses by
391
+ * name rather than fabricating.
392
+ */
393
+ export function rapierPhysicsSystem() {
394
+ return physicsSlot.slot;
395
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.21",
5
+ "version": "0.5.23",
6
6
  "description": "Readable TypeScript game engine and universal host for Three.js, PixiJS, and React games.",
7
7
  "keywords": [
8
8
  "game-engine",
@@ -2,7 +2,7 @@
2
2
  "$comment": "Curated by human/agent review, NOT auto-generated (unlike engine-api.json beside it). Every claim was verified against code — see each entry's `evidence`. Staleness is machine-checked at two granularities: `schemaDirHash` (sha256 over packages/engine/src/asset-formats/) and per-entry `evidenceHashes` (sha256 of each cited evidence FILE — catches drift in files outside the schema dir, which is where most entries' evidence lives; T5.5/§7.2).",
3
3
  "_readme": "Read this before authoring scene JSON or engine code, to avoid the negatives below. Refresh flow after a schema-dir OR evidence-file change: `npx tsx scripts/capabilities-hash.ts --check` (fails naming the stale entry) -> re-review affected entries against current code -> `--write` to record fresh hashes. The pre-commit hook runs --check automatically. Each entry's `evidenceHashes` maps the repo-relative file path parsed off the front of each `evidence[]` citation (before any `:line-range` suffix) to a sha256 of that file's current content.",
4
4
  "schemaDirPath": "packages/engine/src/asset-formats/",
5
- "schemaDirHash": "4b984af5ddc2d4e756db14576a3a267f2d25e0b26490bfb953e614edc635625e",
5
+ "schemaDirHash": "cc141166e16e49380581a0a80f014057b3cf31030df741ce41480a1e8dc8d3f6",
6
6
  "reviewedAt": "2026-07-01",
7
7
  "entries": [
8
8
  {
@@ -11,11 +11,11 @@
11
11
  "status": "unsupported",
12
12
  "claim": "No Continuous Collision Detection (CCD). Fast/small dynamic bodies can tunnel through thin colliders in one step.",
13
13
  "evidence": [
14
- "packages/engine/src/adapter/setup-three-root-adapter.ts (the Rapier home: world init + step; no ccd flag is ever set)"
14
+ "packages/engine/src/world3d-react/rapier-physics-bridge.tsx (the engine's one physics-facing module: observation/debug bridge only — it constructs no world, no bodies, and sets no ccd flag)"
15
15
  ],
16
- "notes": "Grepped packages/engine/src for ccd/CCD/setCcdEnabled: zero matches. The engine constructs no rigid bodies itselfgames build their own descs via ctx.RAPIER and nothing engine-side ever enables ccd. Workaround: keep dynamic-body per-step travel small relative to the thinnest collider, or add intermediate colliders; a game may also call setCcdEnabled on its own bodies directly.",
16
+ "notes": "Grepped packages/engine/src for ccd/CCD/setCcdEnabled: zero matches. The engine constructs no Rapier world or rigid bodies at all physics lives in the game's own tree via @react-three/rapier (<Physics>/<RigidBody>), so CCD is a game choice: @react-three/rapier's `ccd` prop (or setCcdEnabled on a body ref) enables it per body with no engine involvement.",
17
17
  "evidenceHashes": {
18
- "packages/engine/src/adapter/setup-three-root-adapter.ts": "14cef0975f38ecceb25b607ef62232c1002bdd172a3456571f50aa9e39263c0c"
18
+ "packages/engine/src/world3d-react/rapier-physics-bridge.tsx": "03e8b28f7c60884a6fc9f00f488e6e157bc34287670c6cf723ee5e28e7924ce7"
19
19
  }
20
20
  },
21
21
  {
@@ -35,15 +35,17 @@
35
35
  "id": "input-value-model",
36
36
  "area": "input",
37
37
  "status": "supported-with-caveats",
38
- "claim": "F1 (spec §12) added a typed action-value model (digital/scalar/vector2/pointerDelta/pointerPosition, default digital) with real deadzone/combine rules and a valueType-mismatch throw. F2 ('Complete Device Backends') un-rejects mouse_move/gamepad_axis_pair, giving both real readers (mouse_move -> pointerDelta/vector2 off the real mouse-delta accumulator; gamepad_axis_pair -> vector2 off a coupled xAxis/yAxis stick, deadzone-rescaled like gamepad_axis), and adds touch_button/touch_stick (digital/vector2) via setTouchButton/setTouchStick. Real window focus (blur/focus) and pointer-lock state are tracked (requestPointerLock / exitPointerLock) and flush held REAL-device input on loss (#144: virtual/scheduled machine input gates on enabled only and survives blur); gamepad device identity (id/mapping) is exposed via getGamepadInfo/ActionValueSource.gamepadId. F4 adds getLastActiveDevice() + an optional getPrompt device param. Device-poll getters (isDeviceKeyPressed / isDeviceMouseButtonPressed / getDeviceMouseDelta / getDeviceMousePosition / hasDeviceGamepad / isDeviceGamepadButtonPressed / getDeviceGamepadAxis) read the same keysDown/mouseButtons/gamepads stores without inventing implicit actions, and return inert at READ time when inputActive is false. Caveat: pointerPosition still has no real-device backend (test_pointer_position injection only); scalar isn't fed by mouse_move (no axis-select field).",
38
+ "claim": "F1 (spec §12) added a typed action-value model (digital/scalar/vector2/pointerDelta/pointerPosition, default digital) with real deadzone/combine rules and a valueType-mismatch throw. F2 ('Complete Device Backends') un-rejects mouse_move/gamepad_axis_pair, giving both real readers (mouse_move -> pointerDelta/vector2 off the real mouse-delta accumulator; gamepad_axis_pair -> vector2 off a coupled xAxis/yAxis stick, deadzone-rescaled like gamepad_axis), and adds touch_button/touch_stick (digital/vector2) via setTouchButton/setTouchStick. Real window focus (blur/focus) and pointer-lock state are tracked (requestPointerLock / exitPointerLock) and flush held REAL-device input on loss (#144: virtual/scheduled machine input gates on enabled only and survives blur); gamepad device identity (id/mapping) is exposed via getGamepadInfo/ActionValueSource.gamepadId. F4 adds getLastActiveDevice() + an optional getPrompt device param. Device-poll getters (isDeviceKeyPressed / isDeviceMouseButtonPressed / getDeviceMouseDelta / getDeviceMousePosition / hasDeviceGamepad / isDeviceGamepadButtonPressed / getDeviceGamepadAxis) read the same keysDown/mouseButtons/gamepads stores without inventing implicit actions, and return inert at READ time when inputActive is false. getDeviceGamepadAxis is deliberately RAW — no deadzone: the engine's own stick deadzone is on the action path (gamepad_axis / gamepad_axis_pair, where a game declares and can override it), and a device-door caller that wants a stick applies its own engine's processor (unity-compat reproduces Unity's StickDeadzone at 0.125/0.925). The DOM edge those touch setters were written for now ships beside the manager as `input/host-pointer.ts` (`createHostPointer`): the host pointer stream resolved against a render surface, mapped into the game's own design space, with DENSE finger indices and a bounded queue, gated on the manager's own `isInputActive()` (published for exactly that, so a caller never recomposes `isEnabled() && isFocused()` into a looser gate). Caveat on that half: it reports fingers and does not press anything — the control layer (which rect owns which finger, how a knob becomes a stick vector) is the UI's, and no first-party game ships one. Caveat: pointerPosition still has no real-device backend (test_pointer_position injection only); scalar isn't fed by mouse_move (no axis-select field).",
39
39
  "evidence": [
40
40
  "packages/engine/src/input/input-types.ts:2-40 (InputBinding union incl. mouse_move/gamepad_axis_pair/touch_button/touch_stick/test_*)",
41
- "packages/engine/src/input/input-manager.ts (digital/gating/typed getters/device-poll/inject*/setTouch*/getGamepadInfo/F4 getLastActiveDevice+getPrompt/requestPointerLock/exitPointerLock)"
41
+ "packages/engine/src/input/input-manager.ts (digital/gating/typed getters/device-poll/inject*/setTouch*/getGamepadInfo/F4 getLastActiveDevice+getPrompt/requestPointerLock/exitPointerLock)",
42
+ "packages/engine/src/input/host-pointer.ts (createHostPointer: surface/design-space mapping, dense finger indices, isInputActive gate)"
42
43
  ],
43
44
  "notes": "gamepad disconnect/reconnect needs no special-cased code: poll() re-reads navigator.getGamepads() and every read skips a null slot, so a disconnected gamepad's actions go neutral with a correct isJustReleased edge for free — see input-device-backends-f2.test.ts.",
44
45
  "evidenceHashes": {
45
- "packages/engine/src/input/input-types.ts": "62d9005609a72b55a8214c869d1b27825b1a17b9389f3f3f5d0b04725331b382",
46
- "packages/engine/src/input/input-manager.ts": "7d34683759d7ab7fd20ca6f5b5e646eb76a31b00c77977816f4ef863ccb06e62"
46
+ "packages/engine/src/input/input-types.ts": "b21ab448fc16d4881c900c57076ca1269241e1b23f8746fb8daa09c628f00f2b",
47
+ "packages/engine/src/input/input-manager.ts": "3cc14b5d3c5097bddfefbf1d620fc1ba8b0fb1d106191d9c640d94723330e888",
48
+ "packages/engine/src/input/host-pointer.ts": "aa012e3ddbcc236240e394614fb9e470e9ea18952faa848ffc1fa6597a2bdda9"
47
49
  }
48
50
  },
49
51
  {
@@ -57,8 +59,8 @@
57
59
  ],
58
60
  "notes": "This is a compatibility integration, not the canonical animation model: project-owned native controllers may drive AnimationMixer directly. When XState is deliberately used, parallel regions use distinct layer names; the binding filters native clip tracks, uses AnimationUtils.makeClipAdditive, can register with ctx.systems, and publishes format-neutral native runtime inspection until disposal.",
59
61
  "evidenceHashes": {
60
- "packages/engine/src/animation/xstate-animation-meta.ts": "80376ac74819803616c4b396244a971cb9a6b2859c33409d032363bfeb3db44f",
61
- "packages/engine/src/animation/xstate-animation-binding.ts": "8ec400c5abe1550a72adca3dfe30e57c42239fdd1eb986b8e05ec973ea3f3d7b"
62
+ "packages/engine/src/animation/xstate-animation-meta.ts": "d976f58b0bbd8b8159209083075c8860e02a59dd2475e55d9551794b06334035",
63
+ "packages/engine/src/animation/xstate-animation-binding.ts": "984db5596e51a2ea4e20e419f94fbb3af47c1abbadfaf80431bcd2ba4577032c"
62
64
  }
63
65
  },
64
66
  {
@@ -73,7 +75,7 @@
73
75
  "notes": "Limits: ARCHITECTURE-CORE.md. Any editor capability a mount does not reach stands warned per capability until it works.",
74
76
  "evidenceHashes": {
75
77
  "vendor/games/verify-unaltered.mjs": "257a362030a10f4d72a3280815069fbd83f436e1619391635b6f8df2ddd6c339",
76
- "packages/engine/src/manifest/schema.ts": "ce385bc9181b82f9d5044da6a41a025a5cb8ba1fc7156263665080c9fdf2b11c"
78
+ "packages/engine/src/manifest/schema.ts": "1f191ca4d77de85bc9ec4dbe4e5cc83c571dab017ff070ac0d5ee344ebec6fa2"
77
79
  }
78
80
  },
79
81
  {
@@ -83,12 +85,12 @@
83
85
  "claim": "Unmodified native-React DOM games run via the manifest `ingest-react` identity: vendored byte-for-byte (vendor/games/, zero-diff CI), mounted through the react world stack under the host React 19 (deduped). NO authoring in v1 (the editor never writes JSX back into an unmodified game, D-N4).",
84
86
  "evidence": [
85
87
  "vendor/games/verify-unaltered.mjs (vendored-bytes gate: zero UNRECORDED diff — byte-identical files, plus any recorded per-file patch that must reverse-apply to the pinned upstream)",
86
- "packages/editor/src/adapter-resolver.ts"
88
+ "packages/editor/src/roots/react-root.ts"
87
89
  ],
88
90
  "notes": "Scope is native-DOM React ONLY (D-N1): R3F/@pixi/react games are canvas games and ride the three.js/pixi seams instead. Generality limit: ONE proven game (react-rpg — menu-to-dungeon e2e under React 19; both game-2 candidates rejected on evidence, R-N7). Game deps must be host-installed root deps (D-N6). Open R-N6: vendored react games' raw window listeners bypass the play-mode input gate.",
89
91
  "evidenceHashes": {
90
92
  "vendor/games/verify-unaltered.mjs": "257a362030a10f4d72a3280815069fbd83f436e1619391635b6f8df2ddd6c339",
91
- "packages/editor/src/adapter-resolver.ts": "574fb1301f1314af1b268f6fc71e8dcde291620f2f01b5d8d3ce251095d7c775"
93
+ "packages/editor/src/roots/react-root.ts": "68c54cd5426b4480f40e48f11e1445e85697796c280d4f88addbdcd21981cfbd"
92
94
  }
93
95
  },
94
96
  {
@@ -104,7 +106,7 @@
104
106
  "notes": "This file exists for negatives engine-api.json can't express (e.g. 'parses but does nothing'). For the full positive surface, read engine-api.json/.md — regenerated by `npm run generate-engine-manifest`, can't drift.",
105
107
  "evidenceHashes": {
106
108
  "packages/engine/schemas/engine-api.json": "cdfd395fab88575a13c74451051e6c86f8b07980152120f00caa62098ecf7917",
107
- "packages/engine/src/animation/xstate-animation-binding.ts": "8ec400c5abe1550a72adca3dfe30e57c42239fdd1eb986b8e05ec973ea3f3d7b",
109
+ "packages/engine/src/animation/xstate-animation-binding.ts": "984db5596e51a2ea4e20e419f94fbb3af47c1abbadfaf80431bcd2ba4577032c",
108
110
  "packages/engine/src/animation/blend-node.ts": "9e1d2018acf138bb984fd0d2c7ff1045ef00689e613abf531453af0a1ba06b2e"
109
111
  }
110
112
  }
@@ -35,6 +35,7 @@
35
35
  "required": [
36
36
  "version"
37
37
  ],
38
+ "additionalProperties": false,
38
39
  "description": "Engine version pin"
39
40
  },
40
41
  "roots": {
@@ -138,24 +139,6 @@
138
139
  "captureTimeoutMs": {
139
140
  "description": "How long the host waits for this game's first captured frame before the mount FAILS by name. A game that needs a long boot (streamed world assets, a menu the player must clear) raises it; the default is 10s.",
140
141
  "type": "number"
141
- },
142
- "world": {
143
- "description": "The component this root's WORLD is, in the game's own source — a three root whose scene is a React/R3F component tree names it here. Two readers, and they mount different things: the editor mounts THIS component alone as the root's design-time document (a still scene — its frameloop runs ONE bounded settle at mount, so physics-owned bodies rest at their authored spawn, and is then frozen forever; nothing moves in Edit), while `entry` above stays what PLAY mounts (the whole game, its own composition, its own HUD and loop). Declaring it is what lets a scene be a scene in Edit rather than the running game's mount surfaces. A game whose world is not a component omits it and keeps mounting through `entry` in both modes.",
144
- "type": "object",
145
- "properties": {
146
- "entry": {
147
- "type": "string",
148
- "description": "Project-relative path to the game's OWN module exporting that component"
149
- },
150
- "export": {
151
- "description": "Named export to read from that module; omit when the module default-exports it",
152
- "type": "string"
153
- }
154
- },
155
- "required": [
156
- "entry"
157
- ],
158
- "additionalProperties": false
159
142
  }
160
143
  },
161
144
  "additionalProperties": false,
@@ -181,6 +164,24 @@
181
164
  "type": "string"
182
165
  }
183
166
  },
167
+ "world": {
168
+ "description": "The component this root's WORLD is, in the game's own source. Two readers, and they mount different things: the EDITOR mounts THIS component alone as the root's design-time document, while `entry` above stays what PLAY mounts (the whole game — its own composition, its own HUD, its own loop). It sits on the ROOT rather than inside any one adapter because the split it names is not about ingest: a game whose runtime entry is a SHELL over the thing an author actually edits has this shape whether the source is vendored or first-party. Two shipped cases, one field: an ingested game whose `entry` mounts game+HUD+loop while the named export is the world alone; and a translated Unity/Godot port, whose `entry` is a scene HOST that mounts the scene the built player starts on (`EditorBuildSettings` index 0 — routinely a menu with no 3D content at all) while the named export opens the scene an author edits. That is Unity's own split: build settings say where the PLAYER starts; the Scene view opens a scene, and Unity keeps that fact somewhere else entirely. A game whose world is not a separate component omits this and keeps mounting through `entry` in both modes.",
169
+ "type": "object",
170
+ "properties": {
171
+ "entry": {
172
+ "type": "string",
173
+ "description": "Project-relative path to the game's OWN module exporting that component"
174
+ },
175
+ "export": {
176
+ "description": "Named export to read from that module; omit when the module default-exports it",
177
+ "type": "string"
178
+ }
179
+ },
180
+ "required": [
181
+ "entry"
182
+ ],
183
+ "additionalProperties": false
184
+ },
184
185
  "zOrder": {
185
186
  "default": 0,
186
187
  "description": "Canvas stacking order; ties broken by array order",
@@ -256,12 +257,15 @@
256
257
  "id",
257
258
  "label",
258
259
  "roots"
259
- ]
260
+ ],
261
+ "additionalProperties": false
260
262
  }
261
263
  }
262
- }
264
+ },
265
+ "additionalProperties": false
263
266
  }
264
- }
267
+ },
268
+ "additionalProperties": false
265
269
  },
266
270
  "server": {
267
271
  "description": "Multiplayer server config (Colyseus), if this game registers a room",
@@ -305,7 +309,8 @@
305
309
  },
306
310
  "required": [
307
311
  "room"
308
- ]
312
+ ],
313
+ "additionalProperties": false
309
314
  },
310
315
  "resolution": {
311
316
  "description": "Canvas resolution",
@@ -323,7 +328,8 @@
323
328
  "required": [
324
329
  "width",
325
330
  "height"
326
- ]
331
+ ],
332
+ "additionalProperties": false
327
333
  },
328
334
  "rendering": {
329
335
  "description": "Construction-time renderer properties, which no world can declare after the fact. Everything a world CAN declare (tone mapping, output colour space, clear colour, shadow filter) lives on the world's own renderer config instead.",
@@ -350,7 +356,8 @@
350
356
  },
351
357
  "required": [
352
358
  "allowInProduction"
353
- ]
359
+ ],
360
+ "additionalProperties": false
354
361
  },
355
362
  "determinism": {
356
363
  "description": "Seeded-RNG determinism contract (D15) — governs whether ctx.random is boot-seeded from a reproducible seed and whether the gameplay-rng-ban scan / dev-mode Math.random phase trap are active for this project.",
@@ -369,7 +376,8 @@
369
376
  },
370
377
  "required": [
371
378
  "seededRandom"
372
- ]
379
+ ],
380
+ "additionalProperties": false
373
381
  },
374
382
  "budget": {
375
383
  "description": "Performance budget (W3d, F11 perf regression gates) asserted by `vgai perf --assert-budget`: a seeded, fixed-step headless run samples the engine profiler and fails (nonzero exit) when any metric here is exceeded. Only headlessly-measurable metrics are budgetable; real GPU frame time is not (SwiftShader).",
@@ -403,7 +411,8 @@
403
411
  "exclusiveMinimum": 0,
404
412
  "maximum": 9007199254740991
405
413
  }
406
- }
414
+ },
415
+ "additionalProperties": false
407
416
  },
408
417
  "learn": {
409
418
  "description": "Optional learner-facing metadata (FT-5) — set on shipped example projects so the gallery/wizard/Learn-catalog registries can be generated from the manifest itself",
@@ -325,10 +325,8 @@ const AdapterRegionOverlaySchema = z
325
325
  * the full REPLACEMENT, and its entries carry `include`/`mounts` themselves.
326
326
  * Two homes for one fact is how a declaration goes stale in one of them.
327
327
  *
328
- * MEASURED BASIS (PR #2281, verified twice): the region binding was
329
- * replace-only, so no `nativeAdapter()` project could declare a single include
330
- * without restating every region — which is what blocked the source-text R3F
331
- * floor's retirement across the five shipped 3D projects.
328
+ * MEASURED BASIS: a replace-only region binding means no `nativeAdapter()`
329
+ * project can declare a single include without restating every region.
332
330
  */
333
331
  const RegionIncludesSchema = z
334
332
  .record(z.string().min(1), AdapterRegionOverlaySchema)
@@ -18,7 +18,7 @@ export type AdapterSurface = 'three' | 'canvas' | 'dom';
18
18
  * site, not silently contribute nothing or silently default to an existing
19
19
  * kind). Lives here — colocated with the kind vocabulary itself, not in a
20
20
  * generic util module — so the three call sites that need it
21
- * (`runtime/create-runtime.ts`'s mount loop, `editor/adapter-resolver.ts`'s
21
+ * (`runtime/create-runtime.ts`'s mount loop, `editor/binding-resolver.ts`'s
22
22
  * `resolveAllRoots`, `editor/play-mode.ts`'s `installMultiRootAuthoring`)
23
23
  * import it from the same leaf module that defines `AdapterSurface`, keeping the
24
24
  * type and its guard from drifting apart. The `never` parameter is the