@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
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import ts from 'typescript';
19
+ import { indexingIdentifier, propertyKey, selectionTable } from './entrypoint-selection-readers';
19
20
 
20
21
  export type EntrypointSelectionRewrite =
21
22
  | { readonly ok: true; readonly source: string; readonly changed: boolean }
@@ -25,25 +26,6 @@ function parse(path: string, source: string): ts.SourceFile {
25
26
  return ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
26
27
  }
27
28
 
28
- function selectionTable(sf: ts.SourceFile, name: string): ts.ObjectLiteralExpression | undefined {
29
- for (const statement of sf.statements) {
30
- if (!ts.isVariableStatement(statement)) continue;
31
- for (const declaration of statement.declarationList.declarations) {
32
- if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name) continue;
33
- const initializer = declaration.initializer;
34
- if (initializer && ts.isObjectLiteralExpression(initializer)) return initializer;
35
- }
36
- }
37
- return undefined;
38
- }
39
-
40
- function propertyKey(property: ts.ObjectLiteralElementLike): string | undefined {
41
- const name = property.name;
42
- if (!name) return undefined;
43
- if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
44
- return undefined;
45
- }
46
-
47
29
  function tableKeys(table: ts.ObjectLiteralExpression): string[] {
48
30
  const keys: string[] = [];
49
31
  for (const property of table.properties) {
@@ -53,27 +35,6 @@ function tableKeys(table: ts.ObjectLiteralExpression): string[] {
53
35
  return keys;
54
36
  }
55
37
 
56
- /** The identifier in `scenes[activeScene]`, or a failure to read one. */
57
- function indexingIdentifier(sf: ts.SourceFile, selection: string): string | undefined | null {
58
- const names = new Set<string>();
59
- let unreadable = false;
60
- const visit = (node: ts.Node): void => {
61
- if (
62
- ts.isElementAccessExpression(node) &&
63
- ts.isIdentifier(node.expression) &&
64
- node.expression.text === selection
65
- ) {
66
- if (ts.isIdentifier(node.argumentExpression)) names.add(node.argumentExpression.text);
67
- else unreadable = true;
68
- }
69
- ts.forEachChild(node, visit);
70
- };
71
- ts.forEachChild(sf, visit);
72
- if (names.size === 0 && !unreadable) return undefined;
73
- if (names.size !== 1 || unreadable) return null;
74
- return [...names][0];
75
- }
76
-
77
38
  function moduleLevelStringLiteral(sf: ts.SourceFile, name: string): ts.StringLiteral | undefined {
78
39
  for (const statement of sf.statements) {
79
40
  if (!ts.isVariableStatement(statement)) continue;
@@ -43,6 +43,7 @@ import type {
43
43
  ScenesFromEntrypointSelectionParams,
44
44
  } from '../adapter-module';
45
45
  import { EXPORTED_COMPOSITION_REGIONS } from '../adapter-module';
46
+ import { indexingIdentifier, propertyKey, selectionTable } from '../entrypoint-selection-readers';
46
47
  import type { FinderResult } from './finder-result';
47
48
 
48
49
  /** One region's entrypoint, as the host reads it off the manifest + disk. */
@@ -136,54 +137,6 @@ function importedBindings(sf: ts.SourceFile): Map<string, ImportedBinding> {
136
137
  return bindings;
137
138
  }
138
139
 
139
- /** The module-level `const <name> = { … }` initializer, if there is one. */
140
- function selectionTable(sf: ts.SourceFile, name: string): ts.ObjectLiteralExpression | undefined {
141
- for (const statement of sf.statements) {
142
- if (!ts.isVariableStatement(statement)) continue;
143
- for (const declaration of statement.declarationList.declarations) {
144
- if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name) continue;
145
- const initializer = declaration.initializer;
146
- if (initializer && ts.isObjectLiteralExpression(initializer)) return initializer;
147
- }
148
- }
149
- return undefined;
150
- }
151
-
152
- function propertyKey(property: ts.ObjectLiteralElementLike): string | undefined {
153
- const name = property.name;
154
- if (!name) return undefined;
155
- if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
156
- return undefined;
157
- }
158
-
159
- /**
160
- * The identifier the entrypoint INDEXES the selection table with — the
161
- * `activeScene` in `scenes[activeScene]`.
162
- *
163
- * `undefined` when the module never indexes the table, or indexes it with
164
- * anything other than one single identifier: both are honest "cannot be read",
165
- * and the caller turns each into its own note.
166
- */
167
- function indexingIdentifier(sf: ts.SourceFile, selection: string): string | undefined | null {
168
- const names = new Set<string>();
169
- let unreadable = false;
170
- const visit = (node: ts.Node): void => {
171
- if (
172
- ts.isElementAccessExpression(node) &&
173
- ts.isIdentifier(node.expression) &&
174
- node.expression.text === selection
175
- ) {
176
- if (ts.isIdentifier(node.argumentExpression)) names.add(node.argumentExpression.text);
177
- else unreadable = true;
178
- }
179
- ts.forEachChild(node, visit);
180
- };
181
- ts.forEachChild(sf, visit);
182
- if (names.size === 0 && !unreadable) return undefined; // never indexed at all
183
- if (names.size !== 1 || unreadable) return null; // indexed, but not readably
184
- return [...names][0];
185
- }
186
-
187
140
  /**
188
141
  * The string a module-level `const <name> = '…'` binds, or `undefined`.
189
142
  *
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * Host contexts — what the host hands a root when it mounts it, PER SURFACE.
3
3
  *
4
- * This generalizes the old `GameContext`, which baked in first-party system
5
- * choices (a Rapier world, the postprocessing composer, the InputManager, …).
6
- * The host never assumes Rapier/Colyseus; those live behind `SystemAdapters`
7
- * owned by the first-party implementer.
4
+ * The host never assumes Rapier/Colyseus; system choices live behind
5
+ * `SystemAdapters` owned by each root's own implementation.
8
6
  *
9
7
  * ## Why this file is a family, not one interface (P-8)
10
8
  *
@@ -71,18 +69,50 @@
71
69
  * hosts, with no caller anywhere, and its only real effect would have been to
72
70
  * re-open the door this interface exists to close: the host handing a game a
73
71
  * subsystem it did not build. Deleted rather than kept as a promise.
74
- *
75
- * Migration note: `SetupThreeRootAdapter.mount` builds today's `GameContext`
76
- * (Rapier world, composer, …) internally from a `ThreeHostContext`. New code
77
- * targets these contexts; `GameContext` is now a first-party implementation
78
- * detail.
79
72
  */
80
73
 
81
74
  import type * as THREE from 'three';
82
75
  import type { AssetCache } from '../assets';
83
- import type { Game, GameInternal } from '../runtime/game';
76
+ import type { Game } from '../runtime/game';
84
77
  import type { AdapterSurface } from './adapter-surface';
85
78
 
79
+ /**
80
+ * THE FOUR DOORS a mounting root actually opens on the Game — and the whole
81
+ * reason `three`/`canvas` host contexts no longer name `GameInternal`.
82
+ *
83
+ * `GameInternal` is the HOST's own control surface: `runFrame`,
84
+ * `runRenderFrame`, `runTicks`, `registerRoot`, `notifySystemAdaptersChanged`,
85
+ * `dispose`. Handing it to an adapter's `mount()` published the loop driver to
86
+ * the thing being driven — nothing read those members through `host.game`, but
87
+ * the type said they could, so "what may a root do to its Game?" had no
88
+ * answer short of "everything".
89
+ *
90
+ * The four doors, each with its verified consumer:
91
+ *
92
+ * 1. **Input-map load** — `runtime/game-input-seams.ts` calls
93
+ * `game.loadInputMap(path)` (and reads `game.input`/`game.loop.fixedDt`)
94
+ * from every three and canvas mount.
95
+ * 2. **Debug-registry access** — `getDebugRegistry(host.game)`
96
+ * (`r3f-root-factory`, `pixi-react-root-factory`); the registry is keyed by
97
+ * Game IDENTITY, which is why this handle is the Game and not a projection
98
+ * of it.
99
+ * 3. **The Game handed to `<WorldProvider>`** — the react world-state seam
100
+ * (`r3f-root-factory`, `pixi-react-root-factory`, and both react mounts in
101
+ * the editor's resolver). `WorldProvider`'s prop is `Game`.
102
+ * 4. **The profiler toggle** — `host.game.profiler` (`r3f-root-factory`).
103
+ *
104
+ * Doors 2–4 are already on the PUBLIC {@link Game}. `loadInputMap` is the one
105
+ * member that was only on `GameInternal`, and it is explicitly an adapter-mount
106
+ * door (see its own doc comment in `runtime/game.ts`). So this handle is
107
+ * `Game` plus that one method — and a `GameInternal` satisfies it, so every
108
+ * host that already passes one keeps compiling unchanged.
109
+ */
110
+ export interface HostGameHandle extends Game {
111
+ /** Load the game-owned input map ONCE, however many roots ask. See
112
+ * `GameInternal.loadInputMap` for the load-once/competing-paths contract. */
113
+ loadInputMap(path: string, options?: { optional?: boolean }): Promise<void>;
114
+ }
115
+
86
116
  /** The canvas/container the game renders into, plus its size. */
87
117
  export interface HostSurface {
88
118
  readonly canvas: HTMLCanvasElement;
@@ -107,8 +137,8 @@ export interface HostContextBase {
107
137
  * zero-break guarantee for this slice). Typed as the PUBLIC `Game` shell
108
138
  * here because that is the weakest promise any surface makes (a react
109
139
  * root's host has always carried a plain `Game`); the three and canvas
110
- * contexts narrow it to `GameInternal`, which is what their hosts really
111
- * pass.
140
+ * contexts narrow it to {@link HostGameHandle} the four doors a mounting
141
+ * root actually opens, which is what their hosts really pass.
112
142
  */
113
143
  readonly game?: Game | undefined;
114
144
  /**
@@ -134,7 +164,7 @@ export interface ThreeHostContext extends HostContextBase {
134
164
  readonly renderer: THREE.WebGLRenderer;
135
165
  /** Shared GLTF/texture cache — three-flavored today; see the header. */
136
166
  readonly assets: AssetCache;
137
- readonly game?: GameInternal | undefined;
167
+ readonly game?: HostGameHandle | undefined;
138
168
  }
139
169
 
140
170
  /**
@@ -149,7 +179,7 @@ export interface CanvasHostContext extends HostContextBase {
149
179
  readonly width: number;
150
180
  readonly height: number;
151
181
  /** Shared Game root when mounted by the universal multi-root host. */
152
- readonly game?: GameInternal | undefined;
182
+ readonly game?: HostGameHandle | undefined;
153
183
  /** DPR override (T6.1 slice 1, D5 §3 — "one DPR
154
184
  * everywhere"): the roots-path host computes ONE dpr for every stacked
155
185
  * surface and passes it here; omitted -> the canvas adapter's own
@@ -176,7 +206,7 @@ export interface CanvasHostContext extends HostContextBase {
176
206
  * `game` (inherited, and deliberately still optional): a
177
207
  * `default-react` sibling mounted BESIDE an ingest root (`packages/editor/src/
178
208
  * ingest-siblings.ts`) has no native `Game` to hand it — there is no
179
- * first-party `GameContext`/loop for the sibling to join, only the ingested
209
+ * first-party loop for the sibling to join, only the ingested
180
210
  * root's own foreign runtime — so its host carries no `game` at all rather than
181
211
  * fabricating an empty one (anti-shim rule); the sibling mounts its entry
182
212
  * component bare, with no `<WorldProvider>` wrap. `mountOneReactRoot`
@@ -1,23 +1,21 @@
1
1
  /**
2
2
  * Adapter interfaces — the seams the engine host and the editor DEPEND ON.
3
3
  *
4
- * host → RootAdapter ← { SetupThreeRootAdapter, IngestRootAdapter, … }
4
+ * host → RootAdapter ← { r3fRootFactory-built adapters, IngestRootAdapter, … }
5
5
  * editor → AuthoringAdapter ← { LiveThreeAuthoringAdapter, ReactRootAuthoringAdapter, … }
6
6
  * game → SystemAdapters (physics/networking/navigation/audio/camera/debug)
7
7
  *
8
8
  * The first-party Rapier/Colyseus stack is ONE implementer of these
9
- * interfaces, not the engine's vocabulary..
9
+ * interfaces, not the engine's vocabulary.
10
10
  *
11
11
  * **This module is TYPE-ONLY, and must stay that way (P-6).** It used to
12
- * value-export its own implementers — `SetupThreeRootAdapter`/`fromSetup`,
13
- * `createRapierPhysicsAdapter`, `createColyseusNetworkingAdapter`,
14
- * `createNavigationAdapter` — so `import type { AuthoringAdapter } from
15
- * '@engine/adapter'` was the only thing keeping the three/Rapier/Colyseus/
16
- * `.vscn` stack out of a Pixi or React consumer's module graph. A seam that
17
- * ships its implementers is not a seam. Import an implementer from its OWN
18
- * path instead:
12
+ * value-export its own implementers — `createRapierPhysicsAdapter`,
13
+ * `createColyseusNetworkingAdapter`, `createNavigationAdapter` — so
14
+ * `import type { AuthoringAdapter } from '@engine/adapter'` was the only
15
+ * thing keeping the three/Rapier/Colyseus stack out of a Pixi or React
16
+ * consumer's module graph. A seam that ships its implementers is not a
17
+ * seam. Import an implementer from its OWN path instead:
19
18
  *
20
- * `@engine/adapter/setup-three-root-adapter` SetupThreeRootAdapter, fromSetup
21
19
  * `@engine/adapter/rapier-physics-adapter` createRapierPhysicsAdapter
22
20
  * `@engine/adapter/colyseus-networking-adapter` createColyseusNetworkingAdapter
23
21
  * `@engine/adapter/first-party-systems` createNavigationAdapter
@@ -88,12 +86,27 @@ export {
88
86
  AUTHORING_PROVIDER_SHAPES,
89
87
  HIERARCHY_PROVIDER_SHAPE,
90
88
  } from './authoring-seam-contract';
89
+ export type {
90
+ BundledEntrySubset,
91
+ EntryStaticSurface,
92
+ ObservationBinding,
93
+ ProjectBinding,
94
+ ProjectionBinding,
95
+ ProtocolFamily,
96
+ RootBinding,
97
+ RootBindingParts,
98
+ RootDeclaration,
99
+ SubstrateBinding,
100
+ TruthBinding,
101
+ } from './binding';
102
+ export { createRootBinding } from './binding';
91
103
  export type { ColyseusNetworkingConfig } from './colyseus-networking-adapter';
92
104
  export type {
93
105
  CanvasHostContext,
94
106
  DomHostContext,
95
107
  HostContextBase,
96
108
  HostContextFor,
109
+ HostGameHandle,
97
110
  HostSurface,
98
111
  ThreeHostContext,
99
112
  } from './host-context';
@@ -134,7 +147,6 @@ export {
134
147
  SeamEvidenceLedger,
135
148
  seamStageAtLeast,
136
149
  } from './seam-evidence';
137
- export type { MountedSetupThreeRoot, SetupThreeRootConfig } from './setup-three-root-adapter';
138
150
  export type {
139
151
  AudioAdapter,
140
152
  AudioDebugEvent,
@@ -14,9 +14,6 @@
14
14
  * implementation of this contract. An ingested game climbs the same ladder
15
15
  * endpoint by endpoint: capture infers what it can (the scene), the game
16
16
  * declares what inference can't reach (its DOM root, its session lifecycle).
17
- *
18
- * Origin (root), F17+F21 (lifecycle), WO-SYS1 (systems), WO-SYS2
19
- * (`systems.systemAdapters`).
20
17
  */
21
18
 
22
19
  import type {
@@ -31,7 +31,7 @@
31
31
  * classes. A game that bundles its own copy cannot be captured this way (the
32
32
  * module-identity gatekeeper).
33
33
  *
34
- * Wave 17 adds an OPTIONAL, ADDITIVE composer capture: a game rendering through
34
+ * There is an OPTIONAL, ADDITIVE composer capture: a game rendering through
35
35
  * its own three.js addon `EffectComposer`
36
36
  * (`three/examples/jsm/postprocessing/EffectComposer.js`) still trips the `render`
37
37
  * trap above (its `RenderPass` calls `renderer.render(scene,camera)` internally),
@@ -55,10 +55,10 @@
55
55
 
56
56
  import type * as THREE from 'three';
57
57
  import {
58
+ type CaptureWaitOptions,
58
59
  documentVisibilityClock,
59
60
  startVisibleCaptureWindow,
60
61
  type VisibilityClock,
61
- type VisibleCaptureWindow,
62
62
  } from './visible-capture-window';
63
63
 
64
64
  /** The renderer operations the ingest host may lawfully use after capture.
@@ -209,31 +209,28 @@ export interface RenderPassHooks {
209
209
  }
210
210
 
211
211
  /**
212
- * Options for {@link SceneCaptureHandle.waitForCapture}. Passing a bare number
213
- * is the same as `{ timeoutMs }` the shape every existing caller uses.
212
+ * Options for {@link SceneCaptureHandle.waitForCapture} declared with the
213
+ * window they configure (`visible-capture-window.ts`), because they configure
214
+ * the WAIT and not this surface, and re-exported here for the callers that
215
+ * look for them beside `waitForCapture`.
214
216
  */
215
- export interface CaptureWaitOptions {
216
- /** The capture window, in VISIBLE milliseconds (default 10s). */
217
- timeoutMs?: number | undefined;
218
- /** Injected in tests; defaults to the document's own visibility. */
219
- visibility?: VisibilityClock | undefined;
220
- /**
221
- * Called with a LIVE view of the wait when it begins, and with `null` the
222
- * moment it ends (captured, expired, or the window was cancelled).
223
- *
224
- * A wait parked on a hidden tab is otherwise indistinguishable from a hung
225
- * mount: nothing renders, nothing fails, and every door reports silence.
226
- * This is the seam the editor publishes to `vgai status` so the answer is
227
- * "waiting for the first visible frame — the tab is hidden", not a
228
- * countdown that is not running.
229
- */
230
- onWait?: ((wait: VisibleCaptureWindow | null) => void) | undefined;
231
- }
217
+ export type { CaptureWaitOptions };
218
+
219
+ /**
220
+ * WHICH interception yielded the captured renderer. Not decoration: every
221
+ * coverage row the host prints names the mechanism it reached the game
222
+ * through, and the two are materially different — a `shared-three` game runs
223
+ * on the host's very `three` instance, while a `devtools-observer` game runs
224
+ * its OWN pinned revision and only its renderer is shared.
225
+ */
226
+ export type CaptureMechanism = 'shared-three' | 'devtools-observer';
232
227
 
233
228
  /** Handle returned by {@link installSceneCapture}. */
234
229
  export interface SceneCaptureHandle {
235
230
  /** The captured runtime, or null until the game renders its first frame. */
236
231
  readonly captured: CapturedRuntime | null;
232
+ /** How {@link captured} was reached; `null` while nothing is captured. */
233
+ readonly capturedVia: CaptureMechanism | null;
237
234
  /**
238
235
  * Install (or clear, with `null`) hooks bracketing every `render()` the
239
236
  * CAPTURED renderer makes. Set AFTER capture, because the consumer
@@ -259,7 +256,7 @@ export interface SceneCaptureHandle {
259
256
  * host can pause (set null) and resume (re-set it) the game's own loop. */
260
257
  getAnimationLoop(renderer: CapturedThreeRenderer): ((time: number) => void) | null;
261
258
  /**
262
- * Wave 17 (D-C3): resize every captured `EffectComposer` that renders
259
+ * Resize every captured `EffectComposer` that renders
263
260
  * through the captured renderer to `w`×`h`, matching its pixel ratio to
264
261
  * `renderer.getPixelRatio()` — the composer tracks whatever DPR policy the
265
262
  * host already applies to the renderer; no separate knob. A no-op when no
@@ -278,6 +275,123 @@ interface ThreeLike {
278
275
  WebGLRenderer: { prototype: Record<string, unknown> };
279
276
  }
280
277
 
278
+ /**
279
+ * A three renderer instance reached through three's OWN devtools seam, rather
280
+ * than through a prototype the host shares with the game.
281
+ *
282
+ * Only the two constructor-assigned own methods this file already traps on the
283
+ * shared prototype are named — nothing else about a foreign instance is
284
+ * assumed, because nothing else about it is known.
285
+ */
286
+ interface ForeignRendererLike {
287
+ render?: unknown;
288
+ setAnimationLoop?: unknown;
289
+ domElement?: unknown;
290
+ }
291
+
292
+ /** What {@link observeForeignThreeRenderers} needs from the enclosing capture,
293
+ * passed in rather than closed over so this stays a top-level function. */
294
+ interface ForeignObserverPorts {
295
+ /** True when this renderer's `render` was already intercepted by the shared
296
+ * prototype trap — i.e. it belongs to the host's OWN `three`, which is
297
+ * already captured properly and must not be double-wrapped. */
298
+ alreadyTrapped(renderer: object): boolean;
299
+ isHostRenderer(renderer: unknown): boolean;
300
+ /** The same `(self, scene, camera)` observation the prototype trap makes. */
301
+ observeRender(self: unknown, real: (...args: unknown[]) => unknown, args: unknown[]): unknown;
302
+ /** Record a non-null animation-loop callback (hidden-tab pumping). */
303
+ recordLoop(renderer: object, callback: (time: number) => void): void;
304
+ }
305
+
306
+ /**
307
+ * FOREIGN-INSTANCE CAPTURE — through three's own `__THREE_DEVTOOLS__` seam.
308
+ *
309
+ * The prototype trap above can only reach a game that resolves `three` to the
310
+ * host's instance ("the module-identity gatekeeper" in this file's header). A
311
+ * game vendored as a BUILT BUNDLE routinely does not: a webpack/CRA build
312
+ * inlines three outright, and a Vite build externalized to the game's own
313
+ * pinned `three-rNNN.module.js` loads a second ES module. Those games rendered
314
+ * a real picture the host could not see, and the mount failed by name.
315
+ *
316
+ * The canvas lane already answers this exact question and answers it the same
317
+ * way: `authoring/canvas-runtime-recognition.ts` recognizes Phaser and Babylon
318
+ * through THEIR OWN documented public registries (`Phaser.GAMES`,
319
+ * `BABYLON.Engine.Instances`) rather than by sharing a module instance. three's
320
+ * equivalent public registry is `__THREE_DEVTOOLS__`: every `WebGLRenderer`
321
+ * constructor since r118 ends with
322
+ *
323
+ * if ( typeof __THREE_DEVTOOLS__ !== 'undefined' )
324
+ * __THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) );
325
+ *
326
+ * — a bare global lookup, so it is not affected by the game-globals lexical
327
+ * shadow, and it is present in every revision this repo vendors (r129, r155,
328
+ * r170) as well as the host's own r180. Setting that global before the game's
329
+ * modules evaluate therefore hands the host the game's renderer INSTANCE, and
330
+ * an own-property wrapper on that instance's `render` yields the identical
331
+ * `(scene, camera, renderer)` triple the prototype trap yields.
332
+ *
333
+ * This never changes what happens for a deduped game: such a renderer's own
334
+ * `render` assignment was already intercepted by the prototype setter, so
335
+ * `alreadyTrapped` skips it and there is exactly one interception per renderer.
336
+ *
337
+ * Returns its own teardown. If something else already installed
338
+ * `__THREE_DEVTOOLS__` (the real three.js devtools extension), we listen on it
339
+ * and leave it in place.
340
+ */
341
+ function observeForeignThreeRenderers(ports: ForeignObserverPorts): () => void {
342
+ const host = globalThis as { __THREE_DEVTOOLS__?: EventTarget };
343
+ if (typeof EventTarget !== 'function') return () => undefined;
344
+ const weInstalled = host.__THREE_DEVTOOLS__ === undefined;
345
+ if (weInstalled) host.__THREE_DEVTOOLS__ = new EventTarget();
346
+ const target = host.__THREE_DEVTOOLS__;
347
+ if (!target) return () => undefined;
348
+ const restores: Array<() => void> = [];
349
+
350
+ function wrapOwn(instance: object, key: 'render' | 'setAnimationLoop', value: unknown): void {
351
+ const prior = (instance as Record<string, unknown>)[key];
352
+ Object.defineProperty(instance, key, {
353
+ configurable: true,
354
+ writable: true,
355
+ value,
356
+ });
357
+ restores.push(() => {
358
+ Object.defineProperty(instance, key, { configurable: true, writable: true, value: prior });
359
+ });
360
+ }
361
+
362
+ const listener = (event: Event): void => {
363
+ const detail = (event as CustomEvent<unknown>).detail as ForeignRendererLike | null;
364
+ // `observe` also carries Scenes (r129/r155/r170 dispatch from the `Scene`
365
+ // constructor too). A renderer is the one that owns a canvas and a render
366
+ // method; anything else is not this observer's subject.
367
+ if (!detail || typeof detail !== 'object') return;
368
+ if (typeof detail.render !== 'function' || !detail.domElement) return;
369
+ if (ports.alreadyTrapped(detail)) return;
370
+ if (ports.isHostRenderer(detail)) return;
371
+ const realRender = detail.render as (...args: unknown[]) => unknown;
372
+ wrapOwn(detail, 'render', function (this: unknown, ...args: unknown[]) {
373
+ return ports.observeRender(detail, realRender, args);
374
+ });
375
+ const realSal = detail.setAnimationLoop;
376
+ if (typeof realSal === 'function') {
377
+ const sal = realSal as (...args: unknown[]) => unknown;
378
+ wrapOwn(detail, 'setAnimationLoop', function (this: unknown, callback: unknown) {
379
+ if (typeof callback === 'function') {
380
+ ports.recordLoop(detail, callback as (time: number) => void);
381
+ }
382
+ return sal.call(detail, callback);
383
+ });
384
+ }
385
+ };
386
+
387
+ target.addEventListener('observe', listener);
388
+ return () => {
389
+ target.removeEventListener('observe', listener);
390
+ for (const restore of restores.splice(0)) restore();
391
+ if (weInstalled) delete host.__THREE_DEVTOOLS__;
392
+ };
393
+ }
394
+
281
395
  // Minimal structural type for the bits of an `EffectComposer` instance we
282
396
  // touch. This module never imports a concrete `three/examples/jsm/…` addon
283
397
  // either — the caller passes the SAME class its ingested game imports, so the
@@ -300,7 +414,7 @@ interface PrototypeRendererCtorLike {
300
414
  * Install the render accessor trap on `threeNamespace.WebGLRenderer.prototype`.
301
415
  * Pass the host's `three` module so the game (which shares it) is trapped.
302
416
  *
303
- * `effectComposerCtor` is OPTIONAL (Wave 17, D-C2): pass the host's
417
+ * `effectComposerCtor` is OPTIONAL: pass the host's
304
418
  * `EffectComposer` class (`three/examples/jsm/postprocessing/EffectComposer.js`)
305
419
  * to also trap composer construction/rendering on that shared addon, so
306
420
  * `resizeComposers()` can reach it. Omitting it (or a game never constructing
@@ -380,14 +494,7 @@ export function installSceneCapture(
380
494
  get(this: Record<symbol, unknown>) {
381
495
  const self = this;
382
496
  return function setAnimationLoop(this: unknown, cb: unknown) {
383
- if (cb) {
384
- loopCallbacks.set(self as object, cb as (time: number) => void);
385
- loopedRenderers.add(self as object);
386
- // Hidden tabs park rAF. Play/eval still need a first frame, so when
387
- // a waiter is parked we drive the game's own loop once — the same
388
- // class of tick `waitSimTime` already uses for a hidden document.
389
- queueMicrotask(pumpHiddenLoops);
390
- }
497
+ if (cb) recordAnimationLoop(self as object, cb as (time: number) => void);
391
498
  const real = self[REAL_SAL];
392
499
  return typeof real === 'function'
393
500
  ? (real as (...a: unknown[]) => unknown).call(self, cb)
@@ -402,10 +509,13 @@ export function installSceneCapture(
402
509
  * `after` runs in a `finally`, so a throwing game frame still restores
403
510
  * whatever the hook armed.
404
511
  */
405
- function forwardRender(self: Record<symbol, unknown>, args: unknown[]): unknown {
406
- const real = self[REAL] as (...a: unknown[]) => unknown;
512
+ function bracketedRender(
513
+ self: unknown,
514
+ real: (...a: unknown[]) => unknown,
515
+ args: unknown[],
516
+ ): unknown {
407
517
  const hooks = renderPassHooks;
408
- if (hooks === null || !captured || (self as unknown) !== captured.renderer) {
518
+ if (hooks === null || !captured || self !== captured.renderer) {
409
519
  return real.apply(self, args);
410
520
  }
411
521
  hooks.before();
@@ -416,6 +526,20 @@ export function installSceneCapture(
416
526
  }
417
527
  }
418
528
 
529
+ function forwardRender(self: Record<symbol, unknown>, args: unknown[]): unknown {
530
+ return bracketedRender(self, self[REAL] as (...a: unknown[]) => unknown, args);
531
+ }
532
+
533
+ /** Register a game's animation-loop callback, whichever trap saw it. */
534
+ function recordAnimationLoop(renderer: object, callback: (time: number) => void): void {
535
+ loopCallbacks.set(renderer, callback);
536
+ loopedRenderers.add(renderer);
537
+ // Hidden tabs park rAF. Play/eval still need a first frame, so when a
538
+ // waiter is parked we drive the game's own loop once — the same class of
539
+ // tick `waitSimTime` already uses for a hidden document.
540
+ queueMicrotask(pumpHiddenLoops);
541
+ }
542
+
419
543
  /**
420
544
  * Commit to a world, DECLARATION FIRST.
421
545
  *
@@ -510,6 +634,21 @@ export function installSceneCapture(
510
634
  },
511
635
  });
512
636
 
637
+ // A game that does NOT share the host's `three` never reaches the prototype
638
+ // trap above. three's own devtools seam reaches it anyway — see
639
+ // {@link observeForeignThreeRenderers}.
640
+ const foreignRenderers = new WeakSet<object>();
641
+ const stopForeignObserver = observeForeignThreeRenderers({
642
+ alreadyTrapped: (renderer) => (renderer as Record<symbol, unknown>)[REAL] !== undefined,
643
+ isHostRenderer: (renderer) => opts?.isHostRenderer?.(renderer) === true,
644
+ observeRender: (self, real, args) => {
645
+ foreignRenderers.add(self as object);
646
+ observeRendererRender(self, args[0], args[1]);
647
+ return bracketedRender(self, real, args);
648
+ },
649
+ recordLoop: recordAnimationLoop,
650
+ });
651
+
513
652
  // DOM-backed Three renderers (CSS3DRenderer is the first) use a normal
514
653
  // prototype method rather than WebGLRenderer's constructor assignment. Trap
515
654
  // only classes explicitly supplied by the host, and restore each byte-for-byte.
@@ -537,7 +676,7 @@ export function installSceneCapture(
537
676
  });
538
677
  }
539
678
 
540
- // ---- Wave 17 (D-C2): additive composer capture, only when a composer ctor
679
+ // ---- (D-C2): additive composer capture, only when a composer ctor
541
680
  // was passed. `EffectComposer` methods (including `render`) live on the
542
681
  // PROTOTYPE (a plain ES class — the constructor never does `this.render =
543
682
  // …`), so a direct method-wrapper suffices — no getter/setter indirection
@@ -563,7 +702,7 @@ export function installSceneCapture(
563
702
  }
564
703
 
565
704
  // Extracted so `uninstall()`'s own cognitive complexity doesn't grow with
566
- // this additive Wave-17 restore step (biome's noExcessiveCognitiveComplexity).
705
+ // this additive restore step (biome's noExcessiveCognitiveComplexity).
567
706
  function restoreComposerTrap() {
568
707
  if (!composerProto) return;
569
708
  if (priorComposerRender) composerProto['render'] = priorComposerRender;
@@ -574,6 +713,12 @@ export function installSceneCapture(
574
713
  get captured() {
575
714
  return captured;
576
715
  },
716
+ get capturedVia(): CaptureMechanism | null {
717
+ if (!captured) return null;
718
+ return foreignRenderers.has(captured.renderer as unknown as object)
719
+ ? 'devtools-observer'
720
+ : 'shared-three';
721
+ },
577
722
  getDrawCount() {
578
723
  return drawCount;
579
724
  },
@@ -618,8 +763,11 @@ export function installSceneCapture(
618
763
  new Error(
619
764
  `Scene capture timed out after ${timeoutMs}ms of VISIBLE time ` +
620
765
  `(${Math.round(captureWindow.elapsedHiddenMs())}ms browser-suspended, which is ` +
621
- 'not counted because no frame can be presented) — the game never rendered, ' +
622
- 'or it bundles its own (un-shared) copy of three.',
766
+ 'not counted because no frame can be presented) — no three renderer of ANY ' +
767
+ "instance drew a scene: neither the host's own `three` (the shared prototype " +
768
+ "trap) nor a renderer announcing itself on three's `__THREE_DEVTOOLS__` seam, " +
769
+ 'which is what reaches a game that bundles its own copy. The game never ' +
770
+ 'rendered.',
623
771
  ),
624
772
  );
625
773
  },
@@ -675,10 +823,13 @@ export function installSceneCapture(
675
823
  }
676
824
  if (priorSAL) Object.defineProperty(proto, 'setAnimationLoop', priorSAL);
677
825
  else delete proto['setAnimationLoop'];
678
- // Wave 17: fully restore the composer addon's `render` (a plain
826
+ // Fully restore the composer addon's `render` (a plain
679
827
  // prototype method — no per-instance own-property to restore, unlike
680
828
  // the renderer/loop traps above).
681
829
  restoreComposerTrap();
830
+ // Stop listening on three's devtools seam and give every foreign
831
+ // renderer instance its own `render`/`setAnimationLoop` back.
832
+ stopForeignObserver();
682
833
  for (const restore of additionalRendererRestores) restore();
683
834
  },
684
835
  };