@volter/editor-game 0.5.65

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 (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,544 @@
1
+ /**
2
+ * THE loop gate for an ingested game — the SimCity ledger's S-5 ("the loop
3
+ * gate silently no-ops: after pause, the gated game still advances").
4
+ *
5
+ * ## What was actually wrong
6
+ *
7
+ * An ingested game runs in the EDITOR'S OWN page, where nothing can be replaced
8
+ * wholesale without collateral on the editor itself, so the entire "gate" was
9
+ * `createIngestLoopGate`: `renderer.setAnimationLoop(null)`, guarded by a check
10
+ * that the game had ever CALLED `setAnimationLoop`.
11
+ *
12
+ * That check is a declaration, not a measurement, and it covers exactly one
13
+ * loop driver. A game that renders through `setAnimationLoop` (so the check
14
+ * passed and the gate reported success) and simulates through
15
+ * `setInterval(this.simulate, 1000)` parks nothing when
16
+ * `setAnimationLoop(null)` runs. Pause froze the picture and the city kept
17
+ * building.
18
+ *
19
+ * ## The cure: the `gated-globals` precedent, applied to scheduling
20
+ *
21
+ * The dev server already prepends a lexical shadow to every project/game module
22
+ * so `window`/`document` resolve to editor-owned proxies
23
+ * (`game-globals-prelude.ts`). The same prelude now shadows the SCHEDULING
24
+ * functions — `setTimeout`, `setInterval`, `requestAnimationFrame` and their
25
+ * cancels — onto the gate this module builds. Only game modules see them; the
26
+ * editor's own timers are untouched, which is precisely why patching the real
27
+ * globals was never an option.
28
+ *
29
+ * A held gate PARKS callbacks, it does not drop them: a one-shot timer fires on
30
+ * release, an interval fires once (coalesced — a held gate must not release a
31
+ * burst of missed ticks), and frame callbacks replay in order. While OPEN the
32
+ * wrappers are straight pass-throughs that run the callback synchronously in
33
+ * the real timer's own tick, so an ungated session's timing is unchanged.
34
+ *
35
+ * ## Recorded limitation (stated, not hidden)
36
+ *
37
+ * This gate does not freeze `performance.now()`/`Date.now()` — those are the
38
+ * editor's own clocks and it shares them with the game, so a game CAN tell it
39
+ * was held. Parked frame callbacks are handed the gate's virtual timestamp (which
40
+ * is what a `dt` is normally computed from), but a game reading `Date.now()`
41
+ * directly will see the pause. That is a real gap in this rung, and it is
42
+ * better stated than papered over.
43
+ *
44
+ * Everything here is injectable (`createSameRealmLoopGate` takes its
45
+ * schedulers; `verifySameRealmLoopControl` takes the gate, a progress reader
46
+ * and a sleep) so the whole decision is unit-testable with no browser.
47
+ */
48
+
49
+ /**
50
+ * The vocabulary a loop verdict speaks.
51
+ *
52
+ * `'unmeasured'` is a first-class answer, not a failure: the probe only ever
53
+ * concludes from something it OBSERVED, and with nothing due while the gate was
54
+ * held it withheld nothing and therefore learned nothing about who drives this
55
+ * loop. On a hidden or backgrounded tab there is routinely nothing due, which
56
+ * would otherwise make the probe report TAB VISIBILITY as a property of the
57
+ * game.
58
+ */
59
+ export type LoopVerdictValue = 'gated' | 'self-driven' | 'unmeasured';
60
+
61
+ /** What a verdict was concluded FROM — see {@link verifySameRealmLoopControl}
62
+ * for the rule each number participates in. */
63
+ export interface LoopEvidence {
64
+ readonly pendingWhileHeld: number;
65
+ readonly firedWhileHeld: number;
66
+ readonly progressBefore: number;
67
+ readonly progressAfter: number;
68
+ readonly windowMs: number;
69
+ }
70
+
71
+ /**
72
+ * THE PAIR the coverage report and the status facet speak: a verdict WITH the
73
+ * evidence it was concluded from, or `null` for no verdict at all.
74
+ *
75
+ * ## Why it is a pair and not the word
76
+ *
77
+ * `'gated'` and `'self-driven'` are also an AUTHOR'S INTENT WORD — the manifest
78
+ * declares `loop: 'gated' | 'self-driven'` per root
79
+ * (`@volter/editor-project/manifest/schema`). While status and coverage emitted the bare
80
+ * string, a declared intent and a measured verdict were the same two bytes, and
81
+ * two mount routes duly asserted `'gated'` with no probe behind them: the report
82
+ * printed `loop ✓ gated` about games nothing had ever held.
83
+ *
84
+ * The provenance therefore lives in the VALUE'S SHAPE, not beside it. A verdict
85
+ * cannot be spelled without {@link LoopEvidence}, and evidence is producible
86
+ * only by {@link verifySameRealmLoopControl} — so "this was measured" is a
87
+ * structural property a stub cannot accidentally satisfy, and a route with no
88
+ * probe has exactly one thing it can say: `null`, plus its own reason.
89
+ *
90
+ * `readiness.ts`'s `RootReadiness.source: 'declared' | 'measured'` is the
91
+ * precedent for making provenance visible, and this is deliberately the
92
+ * STRONGER form of it rather than a transcription: readiness has two real
93
+ * answerers to tell apart, whereas the manifest's `loop` is
94
+ * `.default('gated')` — an unwritten field and an authored one are the same
95
+ * value by the time anything can read it, so a `source: 'declared'` member here
96
+ * would have to FABRICATE a declaration nobody made (the anti-shim rule). The
97
+ * declared word stays where it is read (mount machinery); it never enters this
98
+ * type.
99
+ */
100
+ export interface MeasuredLoop {
101
+ readonly verdict: 'gated' | 'self-driven';
102
+ readonly evidence: LoopEvidence;
103
+ }
104
+
105
+ /**
106
+ * Fold a probe answer into {@link MeasuredLoop}: a MEASURED verdict keeps its
107
+ * evidence, and `'unmeasured'` (or no probe at all) becomes `null` — the same
108
+ * "not measured yet" every other unprobed seam reports. The probe's own
109
+ * `reason` travels alongside it either way, so a reader always learns why there
110
+ * is no verdict.
111
+ */
112
+ export function measuredLoop(
113
+ verdict: SameRealmLoopVerdict | null | undefined,
114
+ ): MeasuredLoop | null {
115
+ if (!verdict) return null;
116
+ if (verdict.loop !== 'gated' && verdict.loop !== 'self-driven') return null;
117
+ return { verdict: verdict.loop, evidence: verdict.evidence };
118
+ }
119
+
120
+ /** The real scheduling functions the gate wraps. */
121
+ export interface RealSchedulers {
122
+ setTimeout(callback: () => void, ms?: number): number;
123
+ clearTimeout(id: number): void;
124
+ setInterval(callback: () => void, ms?: number): number;
125
+ clearInterval(id: number): void;
126
+ requestAnimationFrame(callback: (time: number) => void): number;
127
+ cancelAnimationFrame(id: number): void;
128
+ now(): number;
129
+ }
130
+
131
+ /** The gated replacements, in the shape game code expects. */
132
+ export interface GatedSchedulers {
133
+ setTimeout(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): number;
134
+ clearTimeout(id: number): void;
135
+ setInterval(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): number;
136
+ clearInterval(id: number): void;
137
+ requestAnimationFrame(callback: (time: number) => void): number;
138
+ cancelAnimationFrame(id: number): void;
139
+ }
140
+
141
+ /** A snapshot of the gate's counters — everything the probe reads. */
142
+ export interface SameRealmLoopGateStats {
143
+ /** `'open'` (callbacks run) · `'held'` (callbacks parked) · `'step'` (one batch, then held). */
144
+ readonly mode: 'open' | 'held' | 'step';
145
+ /** Callbacks currently parked by the gate. */
146
+ readonly pending: number;
147
+ /** Total scheduling calls the game has made through the shadowed functions. */
148
+ readonly requests: number;
149
+ /** Total callbacks the gate has actually dispatched. */
150
+ readonly fired: number;
151
+ /**
152
+ * Callbacks that ran WHILE THE GATE WAS HELD. Structurally always 0 — it is a
153
+ * falsifier, not a statistic: a non-zero value means the gate leaked, and the
154
+ * probe reports `self-driven` rather than trusting itself.
155
+ */
156
+ readonly firedWhileHeld: number;
157
+ /** Real scheduler handles still owned by this gate. Optional for older
158
+ * adapter-provided test doubles; the first-party gate always reports them. */
159
+ readonly activeTimeouts?: number;
160
+ readonly activeIntervals?: number;
161
+ readonly activeAnimationFrames?: number;
162
+ /** Input events observed by the realm's window/document proxies. A blocked
163
+ * event is counted in both `input` and `inputBlocked`. */
164
+ readonly input?: number;
165
+ readonly inputBlocked?: number;
166
+ }
167
+
168
+ export interface SameRealmLoopDisposal {
169
+ readonly timeouts: number;
170
+ readonly intervals: number;
171
+ readonly animationFrames: number;
172
+ readonly parkedCallbacks: number;
173
+ }
174
+
175
+ export interface SameRealmLoopGate {
176
+ /** The functions the prelude shadows onto. */
177
+ readonly schedulers: GatedSchedulers;
178
+ hold(): void;
179
+ release(): void;
180
+ /** Run exactly `n` parked batches, then hold again (editor Step). */
181
+ step(n?: number): void;
182
+ stats(): SameRealmLoopGateStats;
183
+ recordInput?(blocked: boolean): void;
184
+ /** Cancel every callback still owned by the mount. Optional only so
185
+ * third-party/test doubles written before mount-scoped realms remain valid. */
186
+ dispose?(): SameRealmLoopDisposal;
187
+ }
188
+
189
+ interface Parked {
190
+ run(): void;
191
+ /** Set for one-shot timers, so `clearTimeout` can unpark an already-fired one. */
192
+ timerId?: number;
193
+ /** Set for frame callbacks, so `cancelAnimationFrame` can unpark one. */
194
+ frameId?: number;
195
+ }
196
+
197
+ /**
198
+ * Build a loop gate over `real`. The gate starts OPEN — a mount must be allowed
199
+ * to construct and draw before anything holds it.
200
+ */
201
+ export function createSameRealmLoopGate(real: RealSchedulers): SameRealmLoopGate {
202
+ let mode: 'open' | 'held' | 'step' = 'open';
203
+ let stepsLeft = 0;
204
+ let requests = 0;
205
+ let fired = 0;
206
+ let firedWhileHeld = 0;
207
+ let input = 0;
208
+ let inputBlocked = 0;
209
+ let heldAt = 0;
210
+ let offset = 0;
211
+ let disposed = false;
212
+ const parked: Parked[] = [];
213
+ const activeTimeouts = new Set<number>();
214
+ const activeIntervals = new Set<number>();
215
+ const activeAnimationFrames = new Set<number>();
216
+ /** Interval id → its parked entry, so a held interval coalesces to ONE
217
+ * pending invocation instead of releasing a burst of missed ticks. */
218
+ const parkedIntervals = new Map<number, Parked>();
219
+
220
+ /** The gate's virtual time — frozen while held. Handed to parked frame
221
+ * callbacks so their `dt` does not swallow the whole pause. */
222
+ const virtualNow = (): number => (mode === 'held' ? heldAt : real.now()) - offset;
223
+
224
+ const dispatch = (entry: Parked): void => {
225
+ fired++;
226
+ if (mode === 'held') firedWhileHeld++;
227
+ try {
228
+ entry.run();
229
+ } catch (err) {
230
+ // A throwing game callback must not swallow the rest of the batch or
231
+ // wedge the gate; rethrow asynchronously so window.onerror still sees it.
232
+ real.setTimeout(() => {
233
+ throw err;
234
+ }, 0);
235
+ }
236
+ };
237
+
238
+ /** Run one parked batch. Callbacks scheduled BY the batch run normally (the
239
+ * gate is open by then). */
240
+ const flush = (): void => {
241
+ const batch = parked.splice(0, parked.length);
242
+ parkedIntervals.clear();
243
+ for (const entry of batch) dispatch(entry);
244
+ };
245
+
246
+ const park = (entry: Parked): void => {
247
+ parked.push(entry);
248
+ };
249
+
250
+ /** The one place "held?" is decided, for every kind of callback. */
251
+ const gated = (entry: Omit<Parked, 'run'> & { run(): void }) => (): void => {
252
+ if (mode === 'held') {
253
+ park(entry);
254
+ return;
255
+ }
256
+ dispatch(entry);
257
+ };
258
+
259
+ const hold = (): void => {
260
+ if (mode === 'held') return;
261
+ heldAt = real.now();
262
+ mode = 'held';
263
+ };
264
+
265
+ const reopen = (): void => {
266
+ if (mode !== 'held') return;
267
+ offset += real.now() - heldAt;
268
+ };
269
+
270
+ const schedulers: GatedSchedulers = {
271
+ setTimeout(callback, ms, ...args) {
272
+ requests++;
273
+ if (disposed) return -1;
274
+ const entry: Parked = { run: () => callback(...args) };
275
+ let id = -1;
276
+ const run = gated(entry);
277
+ id = real.setTimeout(() => {
278
+ activeTimeouts.delete(id);
279
+ run();
280
+ }, ms);
281
+ activeTimeouts.add(id);
282
+ entry.timerId = id;
283
+ return id;
284
+ },
285
+ clearTimeout(id) {
286
+ real.clearTimeout(id);
287
+ activeTimeouts.delete(id);
288
+ const index = parked.findIndex((entry) => entry.timerId === id);
289
+ if (index !== -1) parked.splice(index, 1);
290
+ },
291
+ setInterval(callback, ms, ...args) {
292
+ requests++;
293
+ if (disposed) return -1;
294
+ let id = -1;
295
+ const tick = (): void => {
296
+ const entry: Parked = { run: () => callback(...args) };
297
+ if (mode === 'held') {
298
+ // Coalesce: replace this interval's pending invocation rather than
299
+ // stacking one per missed tick.
300
+ const previous = parkedIntervals.get(id);
301
+ if (previous) {
302
+ const index = parked.indexOf(previous);
303
+ if (index !== -1) parked.splice(index, 1);
304
+ }
305
+ parkedIntervals.set(id, entry);
306
+ park(entry);
307
+ return;
308
+ }
309
+ dispatch(entry);
310
+ };
311
+ id = real.setInterval(tick, ms);
312
+ activeIntervals.add(id);
313
+ return id;
314
+ },
315
+ clearInterval(id) {
316
+ real.clearInterval(id);
317
+ activeIntervals.delete(id);
318
+ const entry = parkedIntervals.get(id);
319
+ if (entry) {
320
+ parkedIntervals.delete(id);
321
+ const index = parked.indexOf(entry);
322
+ if (index !== -1) parked.splice(index, 1);
323
+ }
324
+ },
325
+ requestAnimationFrame(callback) {
326
+ requests++;
327
+ if (disposed) return -1;
328
+ const entry: Parked = { run: () => callback(virtualNow()) };
329
+ let id = -1;
330
+ const run = gated(entry);
331
+ id = real.requestAnimationFrame(() => {
332
+ activeAnimationFrames.delete(id);
333
+ run();
334
+ });
335
+ activeAnimationFrames.add(id);
336
+ entry.frameId = id;
337
+ return id;
338
+ },
339
+ cancelAnimationFrame(id) {
340
+ real.cancelAnimationFrame(id);
341
+ activeAnimationFrames.delete(id);
342
+ const index = parked.findIndex((entry) => entry.frameId === id);
343
+ if (index !== -1) parked.splice(index, 1);
344
+ },
345
+ };
346
+
347
+ return {
348
+ schedulers,
349
+ hold,
350
+ release() {
351
+ if (disposed) return;
352
+ reopen();
353
+ mode = 'open';
354
+ flush();
355
+ },
356
+ step(n = 1) {
357
+ if (disposed) return;
358
+ reopen();
359
+ stepsLeft = n > 0 ? n : 1;
360
+ mode = 'step';
361
+ while (stepsLeft > 0 && parked.length > 0) {
362
+ stepsLeft--;
363
+ flush();
364
+ }
365
+ mode = 'held';
366
+ heldAt = real.now();
367
+ },
368
+ stats() {
369
+ return {
370
+ mode,
371
+ pending: parked.length,
372
+ requests,
373
+ fired,
374
+ firedWhileHeld,
375
+ activeTimeouts: activeTimeouts.size,
376
+ activeIntervals: activeIntervals.size,
377
+ activeAnimationFrames: activeAnimationFrames.size,
378
+ input,
379
+ inputBlocked,
380
+ };
381
+ },
382
+ recordInput(blocked) {
383
+ input++;
384
+ if (blocked) inputBlocked++;
385
+ },
386
+ dispose() {
387
+ if (disposed) {
388
+ return { timeouts: 0, intervals: 0, animationFrames: 0, parkedCallbacks: 0 };
389
+ }
390
+ disposed = true;
391
+ const report: SameRealmLoopDisposal = {
392
+ timeouts: activeTimeouts.size,
393
+ intervals: activeIntervals.size,
394
+ animationFrames: activeAnimationFrames.size,
395
+ parkedCallbacks: parked.length,
396
+ };
397
+ for (const id of activeTimeouts) real.clearTimeout(id);
398
+ for (const id of activeIntervals) real.clearInterval(id);
399
+ for (const id of activeAnimationFrames) real.cancelAnimationFrame(id);
400
+ activeTimeouts.clear();
401
+ activeIntervals.clear();
402
+ activeAnimationFrames.clear();
403
+ parked.length = 0;
404
+ parkedIntervals.clear();
405
+ stepsLeft = 0;
406
+ mode = 'held';
407
+ heldAt = real.now();
408
+ return report;
409
+ },
410
+ };
411
+ }
412
+
413
+ /** The measured verdict for a mount's loop. */
414
+ export interface SameRealmLoopVerdict {
415
+ readonly loop: LoopVerdictValue;
416
+ readonly reason: string;
417
+ readonly evidence: LoopEvidence;
418
+ }
419
+
420
+ /**
421
+ * THE PROBE. Precondition: the caller has already held the gate.
422
+ *
423
+ * `loop: 'gated'` is a claim that something here actually
424
+ * controls this game's frames, so it must be MEASURED:
425
+ *
426
+ * - **Are we withholding anything?** `pending > 0` means the game asked for a
427
+ * frame or a timer and the gate is sitting on it. With nothing parked there
428
+ * is nothing to prove control OVER. This is the rule the old
429
+ * `getAnimationLoop(renderer) !== null` check lacked: it asked whether the
430
+ * game had ever *declared* a loop, not whether we were holding one.
431
+ * - **Did anything run anyway?** `firedWhileHeld` must be 0 (the gate leaked
432
+ * if not), and the injected `progress` reader — the game renderer's own
433
+ * frame counter, which is independent of this gate — must not advance.
434
+ *
435
+ * All three must hold to report `gated`.
436
+ *
437
+ * The window is SAMPLED, not measured end-to-end, and it is long by default.
438
+ * A 250ms window is sized for `requestAnimationFrame`, which fires
439
+ * every ~16ms; the slowest driver here is a `setInterval` a game chooses the
440
+ * period of, and a city-builder's simulation tick is 1000ms. Measured live: a 250ms
441
+ * single-sample window saw `pending: 0` and honestly-but-uselessly reported
442
+ * "the gate is withholding nothing" about a gate that had, moments later, both
443
+ * of that game's intervals parked. A probe reporting its own impatience as the
444
+ * game's shape is the same class of error as the declaration check it replaced,
445
+ * so the window covers a 1Hz driver and every sample counts.
446
+ */
447
+ export async function verifySameRealmLoopControl(opts: {
448
+ gate: SameRealmLoopGate | null;
449
+ /** An independent, monotonically-advancing progress signal for this game —
450
+ * in practice `renderer.info.render.frame` on the CAPTURED game renderer. */
451
+ progress: () => number;
452
+ /** Total real time to watch. Default 1500ms — long enough for a 1Hz
453
+ * `setInterval` simulation tick to become observable (see the doc comment). */
454
+ windowMs?: number;
455
+ /** How often to sample within the window. */
456
+ sampleMs?: number;
457
+ sleep?: (ms: number) => Promise<void>;
458
+ }): Promise<SameRealmLoopVerdict> {
459
+ const windowMs = opts.windowMs ?? 1500;
460
+ const sampleMs = Math.max(1, Math.min(opts.sampleMs ?? 50, windowMs));
461
+ const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
462
+ const gate = opts.gate;
463
+ if (!gate) {
464
+ return {
465
+ loop: 'self-driven',
466
+ reason:
467
+ 'no same-realm loop gate is installed, so nothing here controls this game’s scheduling',
468
+ evidence: {
469
+ pendingWhileHeld: 0,
470
+ firedWhileHeld: 0,
471
+ progressBefore: 0,
472
+ progressAfter: 0,
473
+ windowMs,
474
+ },
475
+ };
476
+ }
477
+ const before = gate.stats();
478
+ const progressBefore = opts.progress();
479
+ let after = before;
480
+ let progressAfter = progressBefore;
481
+ // The PEAK parked count across the window, not the endpoint: a one-shot timer
482
+ // parked at 200ms and released by a Step at 900ms would read 0 at the end and
483
+ // erase the very evidence it is.
484
+ let pendingWhileHeld = before.pending;
485
+ let heldThroughout = before.mode === 'held';
486
+ for (let elapsed = 0; elapsed < windowMs; elapsed += sampleMs) {
487
+ await sleep(Math.min(sampleMs, windowMs - elapsed));
488
+ after = gate.stats();
489
+ progressAfter = opts.progress();
490
+ pendingWhileHeld = Math.max(pendingWhileHeld, after.pending);
491
+ if (after.mode !== 'held') {
492
+ heldThroughout = false;
493
+ break;
494
+ }
495
+ }
496
+ const firedWhileHeld = after.firedWhileHeld - before.firedWhileHeld;
497
+ const evidence = { pendingWhileHeld, firedWhileHeld, progressBefore, progressAfter, windowMs };
498
+
499
+ if (!heldThroughout || after.mode !== 'held') {
500
+ return {
501
+ loop: 'unmeasured',
502
+ reason:
503
+ 'the gate was not held for the whole probe window — something else released it, so ' +
504
+ 'control was not measured',
505
+ evidence,
506
+ };
507
+ }
508
+ if (firedWhileHeld > 0) {
509
+ return {
510
+ loop: 'self-driven',
511
+ reason:
512
+ `${firedWhileHeld} gated callback(s) ran while the gate was held — the gate leaked, ` +
513
+ 'so it cannot claim control',
514
+ evidence,
515
+ };
516
+ }
517
+ if (progressAfter > progressBefore) {
518
+ return {
519
+ loop: 'self-driven',
520
+ reason:
521
+ `the game kept rendering while its scheduling was withheld ` +
522
+ `(frame ${progressBefore} → ${progressAfter} in ${windowMs}ms) — something outside this ` +
523
+ 'gate drives its loop',
524
+ evidence,
525
+ };
526
+ }
527
+ if (pendingWhileHeld === 0) {
528
+ return {
529
+ loop: 'unmeasured',
530
+ reason:
531
+ 'nothing was due while the gate was held, so it withheld nothing and the probe learned ' +
532
+ 'nothing about this loop — note a hidden or backgrounded tab schedules no frames at ' +
533
+ 'all, which is a fact about the TAB, never about the game',
534
+ evidence,
535
+ };
536
+ }
537
+ return {
538
+ loop: 'gated',
539
+ reason:
540
+ `verified: ${pendingWhileHeld} scheduled callback(s) withheld, none ran, and no frame ` +
541
+ `advanced in ${windowMs}ms of held time`,
542
+ evidence,
543
+ };
544
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * WHEN THE EDITOR'S OWN RENDERER CANNOT DRAW AN ADOPTED WORLD.
3
+ *
4
+ * The Scene view draws whatever world the editor has adopted with the EDITOR's
5
+ * `three`. For a first-party project, and for an ingested game that shares the
6
+ * host's instance, that is the same three and the question never arises. It
7
+ * arises for a game captured through three's devtools seam, which runs its own
8
+ * pinned revision: `cuberun` pins r129, and r180's `WebGLRenderer` calls
9
+ * `material.onBeforeRender()` — a method r129 materials do not have.
10
+ *
11
+ * Measured, before this guard: `TypeError: material.onBeforeRender is not a
12
+ * function`, ×8955 and climbing, one per frame, from a mount that had otherwise
13
+ * succeeded completely. The unresolved console set is meant to be a work list;
14
+ * a per-frame throw turns it into noise and buries everything else.
15
+ *
16
+ * THE DISCRIMINATOR IS THE FAILURE, NOT THE INSTANCE. `scene instanceof
17
+ * THREE.Scene` would have been the tidy test and it is the WRONG one: `tanks`
18
+ * also runs its own three (r170) and the host renderer draws its world
19
+ * perfectly — measured, in the editor viewport. Revision distance is not a
20
+ * predicate anyone can evaluate ahead of time, so this waits for the actual
21
+ * throw, attributes it to that scene, and stops re-throwing it forever.
22
+ *
23
+ * The game is NOT lost when this fires: its own renderer keeps drawing into its
24
+ * own canvas, which the host adopted, so the Game tab and `vgai screenshot`
25
+ * show the real thing. What is lost is the editor-camera Scene view of it,
26
+ * which is exactly what the message says.
27
+ */
28
+
29
+ import { commandLine } from '@volter/editor-core/product-command';
30
+ import { editorConsole } from '@volter/editor-core/editor-console';
31
+
32
+ const undrawable = new WeakMap<object, string>();
33
+
34
+ /**
35
+ * Draw `scene`, unless a previous frame proved the host renderer cannot.
36
+ * Returns `true` when the draw ran.
37
+ */
38
+ export function drawSceneUnlessRefused(
39
+ scene: object | null | undefined,
40
+ draw: () => void,
41
+ ): boolean {
42
+ if (!scene) {
43
+ draw();
44
+ return true;
45
+ }
46
+ if (undrawable.has(scene)) return false;
47
+ try {
48
+ draw();
49
+ return true;
50
+ } catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ undrawable.set(scene, message);
53
+ // WARN, not error: this is a standing capability gap with its closing
54
+ // condition named, the shape every other unreached-capability line here
55
+ // takes — not a failure the reader is being asked to go fix in the editor.
56
+ editorConsole.warn(
57
+ "The Scene view cannot draw this world with the editor's own renderer, and has stopped " +
58
+ `trying: ${message}. This world was built by a copy of three the editor did not make ` +
59
+ '(a vendored bundle pins its own revision), and the editor draws it with ITS three — ' +
60
+ 'across a wide enough revision gap the two disagree about the renderer/material ' +
61
+ 'contract. The game itself is unaffected and still drawing: its own canvas is what the ' +
62
+ `Game tab and ${commandLine('screenshot')} show, and hierarchy, selection and inspection all still ` +
63
+ "read the live world. It closes when the game resolves `three` to the host's instance " +
64
+ "— a bare, un-rewritten `import 'three'` in its own source.",
65
+ 'ingest',
66
+ );
67
+ return false;
68
+ }
69
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Public project-tool contract.
3
+ *
4
+ * A tool is an ordinary registered function with a WIRE CONTRACT: declared
5
+ * input/result schemas, structured error codes, and the metadata a caller reads
6
+ * before invoking (`permission`, `host`, `mutates`, `supportsDryRun`). The
7
+ * contract exists because a tool is called across a process boundary — CLI to
8
+ * editor to node host, and `vgai mcp` to an external agent over stdio — where
9
+ * you cannot throw. `ToolRegistry.dispatch` turns every expected failure
10
+ * (unknown name, bad input, an `impl` throw, a bad return) into a typed outcome
11
+ * instead.
12
+ *
13
+ * This module is the surface every consumer imports. It re-exports rather than
14
+ * renames: there is ONE vocabulary, and it is this one.
15
+ */
16
+ export { ToolError } from '@volter/editor-sdk/tools/errors';
17
+ export {
18
+ defineTool,
19
+ type ToolDefinition,
20
+ type ToolErrorDefinition,
21
+ type ToolOutcome,
22
+ ToolRegistry,
23
+ type ToolSummary,
24
+ } from '@volter/editor-sdk/tools/registry';
25
+ export type {
26
+ ExecutionHost as ToolHost,
27
+ ExecutionRequirements as ToolRequirements,
28
+ PermissionMetadata as ToolPermission,
29
+ PermissionRisk as ToolPermissionRisk,
30
+ ToolContext,
31
+ } from '@volter/editor-sdk/tools/types';