@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,150 @@
1
+ /**
2
+ * NAVIGATION, REFUSED OUT LOUD — the fourth thing a page-owning game does that
3
+ * the editor cannot survive.
4
+ *
5
+ * A game that owns its tab is entitled to leave it: a main menu with a "quit"
6
+ * entry assigns `window.location.href`.
7
+ * Run in-realm, that assignment navigates the EDITOR away — the whole session,
8
+ * every other root, and any unsaved authoring with it. It is one line of
9
+ * ordinary game code and there is no cooperative way for a game to know better.
10
+ *
11
+ * So the game realm's `location` is a facade: reads of the game's own
12
+ * query keep working, host control-plane keys (`play`, `project`, `vgai-*`,
13
+ * …) are stripped from `search`/`href` (bubbo's `getUrlParam('play')` is a
14
+ * debug skip-to-Game; the editor's `?play=1` autoplay is not that flag),
15
+ * and every write that would NAVIGATE is refused with a `console.error`
16
+ * naming the game, the exact assignment, and this mechanism. Refused, never
17
+ * swallowed — a silent no-op would make "the quit button does nothing" an
18
+ * unexplainable bug, and the console line is a real product door
19
+ * (`vgai status` reports page console errors).
20
+ *
21
+ * `hash` is deliberately NOT refused: it changes the URL without leaving the
22
+ * document, and games use it for their own state.
23
+ */
24
+
25
+ /** The `Location` properties whose assignment leaves the current document. */
26
+ const NAVIGATING_PROPS = new Set<string>([
27
+ 'href',
28
+ 'protocol',
29
+ 'host',
30
+ 'hostname',
31
+ 'port',
32
+ 'pathname',
33
+ 'search',
34
+ ]);
35
+
36
+ /** The `Location` methods that leave the current document. */
37
+ const NAVIGATING_METHODS = new Set<string>(['assign', 'replace', 'reload']);
38
+
39
+ /** Query keys the HOST owns. A game reading `location.search` must not see
40
+ * them: bubbo treats `play` as "skip the title and start the match." */
41
+ const HOST_SEARCH_KEYS = new Set([
42
+ 'play',
43
+ 'project',
44
+ 'hub',
45
+ 'scene',
46
+ 'ingest',
47
+ 'create-from',
48
+ 'checkout_id',
49
+ ]);
50
+
51
+ function isHostSearchKey(key: string): boolean {
52
+ return HOST_SEARCH_KEYS.has(key) || key.startsWith('vgai');
53
+ }
54
+
55
+ /** The search string a game is allowed to read — host keys removed. */
56
+ export function gameVisibleSearch(search: string): string {
57
+ const raw = search.startsWith('?') ? search.slice(1) : search;
58
+ if (!raw) return '';
59
+ const kept = new URLSearchParams();
60
+ for (const [key, value] of new URLSearchParams(raw)) {
61
+ if (!isHostSearchKey(key)) kept.append(key, value);
62
+ }
63
+ const qs = kept.toString();
64
+ return qs.length > 0 ? `?${qs}` : '';
65
+ }
66
+
67
+ function gameVisibleHref(href: string): string {
68
+ try {
69
+ const url = new URL(href);
70
+ url.search = gameVisibleSearch(url.search);
71
+ return url.href;
72
+ } catch {
73
+ return href;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Wrap a real `Location` so game code can read its own query but never
79
+ * navigate with it, and never see the host's control-plane keys.
80
+ * `refuse` receives a human description of exactly what was attempted.
81
+ */
82
+ export function guardedGameLocation(
83
+ real: Location,
84
+ refuse: (attempt: string) => void,
85
+ options?: {
86
+ /**
87
+ * `location.reload()` translated to what it MEANS on this surface. A
88
+ * page-owning game reloading its window is restarting ITSELF (cuberun's
89
+ * death-screen RESTART is literally `window.location.reload()`), and the
90
+ * realm's window IS the pane — so when the host can restart the game, the
91
+ * idiom is honored rather than refused. Returns true when handled; false
92
+ * falls back to the loud refusal. Measured: a human tester played cuberun
93
+ * and reported RESTART 'does nothing' (runhuman, 2026-08-28).
94
+ */
95
+ readonly reloadGame?: () => boolean;
96
+ },
97
+ ): Location {
98
+ // The proxy TARGET is a plain object, NEVER the real Location. `assign`,
99
+ // `replace` and `reload` are unforgeable own data properties (non-writable,
100
+ // non-configurable) on a real Location, and the proxy invariant for such
101
+ // properties requires the get trap to return the REAL value — so a proxy
102
+ // over the real object THROWS TypeError on every `location.reload` access
103
+ // instead of running the trap. Measured on cuberun's death-screen RESTART
104
+ // (`window.location.reload()`): the click dispatched, the handler died on
105
+ // its first property read, and nothing on screen said why — meaning the
106
+ // guard's refusal path for these three methods had NEVER fired on any
107
+ // tier. An empty target has no own properties, so every trap is free.
108
+ const dummy = Object.create(null) as Location;
109
+ return new Proxy(dummy, {
110
+ get(_target, prop) {
111
+ if (typeof prop === 'string' && NAVIGATING_METHODS.has(prop)) {
112
+ return (...args: unknown[]): void => {
113
+ if (prop === 'reload' && options?.reloadGame?.()) return;
114
+ refuse(`location.${prop}(${args.map((a) => String(a)).join(', ')})`);
115
+ };
116
+ }
117
+ if (prop === 'search') return gameVisibleSearch(String(Reflect.get(real, 'search') ?? ''));
118
+ if (prop === 'href') return gameVisibleHref(String(Reflect.get(real, 'href') ?? ''));
119
+ const value = Reflect.get(real, prop);
120
+ return typeof value === 'function'
121
+ ? (value as (...a: unknown[]) => unknown).bind(real)
122
+ : value;
123
+ },
124
+ set(_target, prop, value) {
125
+ if (typeof prop === 'string' && NAVIGATING_PROPS.has(prop)) {
126
+ refuse(`location.${prop} = ${String(value)}`);
127
+ // Report success to the game: an assignment that THROWS would abort
128
+ // whatever routine tried it (the game's own menu handler), which is a
129
+ // second failure on top of the one we just prevented. The console line
130
+ // above is what makes this non-silent.
131
+ return true;
132
+ }
133
+ return Reflect.set(real, prop, value);
134
+ },
135
+ has(_target, prop) {
136
+ return Reflect.has(real, prop);
137
+ },
138
+ });
139
+ }
140
+
141
+ /** The one message shape for a refused navigation, so every surface that
142
+ * reports one reads the same. */
143
+ export function refusedNavigationMessage(attempt: string): string {
144
+ return (
145
+ `Ingested game attempted to navigate the editor away (${attempt}) — refused. ` +
146
+ "A game running in-realm shares the editor's document, so leaving it would take the " +
147
+ 'whole session with it; game code sees a scoped `location` facade whose reads pass ' +
148
+ 'through and whose navigating writes do not (packages/editor/src/game-location-guard.ts).'
149
+ );
150
+ }
@@ -0,0 +1,196 @@
1
+ import { commandLine } from '@volter/editor-core/product-command';
2
+ /**
3
+ * THE MODULE LANE of `vgai eval` — reach the RUNNING mount's own module
4
+ * instances, by served path, using URLs retained by their owning realm.
5
+ *
6
+ * WHY THE MOUNT STAMP IS LOAD-BEARING. Project modules are served per mount
7
+ * as `/<path>?vgai-mount=<id>` (`project-module-instance.ts` propagates the
8
+ * id through the whole project subtree), so browser module identity is
9
+ * per-instance BY URL. A bare `import('/src/sim/host.ts')` therefore loads a
10
+ * SECOND, never-ticked copy — the phantom-module trap the old string-command
11
+ * registry existed to dodge. Importing through the ACTIVE mount's stamped
12
+ * url returns the exact instance the game is running, from the browser's own
13
+ * module cache.
14
+ *
15
+ * This is a DEV-SERVER door: exported builds are bundles with no module
16
+ * urls, and their curated surface is the adapter's own exports. Errors here
17
+ * teach the contract instead of leaving it to inference.
18
+ */
19
+
20
+ // DEPENDENCY-LIGHT ON PURPOSE: project tool contributions import this
21
+ // module for their faces, and a project's `tsc` walks its imports — pulling
22
+ // `play-mode` (and through it half the editor) breaks every game's own
23
+ // typecheck. The focused-instance read is therefore INJECTED by the editor
24
+ // at boot rather than imported, and the query key is inlined as a constant
25
+ // with a guard test on the one true owner.
26
+ /** Mirrors `project-module-url.ts`'s PROJECT_MOUNT_QUERY (guarded by test —
27
+ * the two must never drift). */
28
+ const PROJECT_MOUNT_QUERY = 'vgai-mount';
29
+
30
+ let focusedInstanceProvider: (() => string) | null = null;
31
+ /** The packaged shell and source-served contributions share the host realm owner. */
32
+ function moduleUrls(instanceId: string): readonly string[] {
33
+ const host = globalThis as typeof globalThis & {
34
+ __vgaiGameModuleUrls?: (id: string) => readonly string[];
35
+ };
36
+ return host.__vgaiGameModuleUrls?.(instanceId) ?? [];
37
+ }
38
+
39
+ /** Wired once by the editor shell (`command-listener.ts`) at boot. */
40
+ export function setFocusedInstanceProvider(provider: () => string): void {
41
+ focusedInstanceProvider = provider;
42
+ }
43
+
44
+ /** Validate/normalize a project-relative module path. Exported pure for the
45
+ * unit test; the rules are the error message. */
46
+ export function normalizeGameModulePath(path: string): string {
47
+ const clean = path.replace(/^\/+/, '');
48
+ if (!clean.startsWith('src/') || /[?#]/.test(clean)) {
49
+ throw new Error(
50
+ `modules("${path}"): pass the project-relative served path, starting "src/" with its real ` +
51
+ 'extension and no query — e.g. modules("src/sim/host.ts").',
52
+ );
53
+ }
54
+ return clean;
55
+ }
56
+
57
+ /**
58
+ * Every project module the mount has ACTUALLY LOADED, as the exact
59
+ * project-relative strings {@link importGameModule} accepts — `src/world.tsx`,
60
+ * not the `/@fs/…` pathname the browser fetched. This is the answer to "what
61
+ * can I reach?", and it is read from URLs recorded by module evaluation.
62
+ *
63
+ * One owner, two readers: the `modules.loaded` listing below and the
64
+ * not-loaded error message. They used to disagree — the error listed raw
65
+ * `/@fs/…` pathnames while its own first sentence told you to pass
66
+ * `src/…`, so the fix it printed was not the fix it named.
67
+ */
68
+ export function loadedGameModulePaths(instanceId: string): string[] {
69
+ const paths = moduleUrls(instanceId).flatMap((name) => {
70
+ const { pathname, searchParams } = new URL(name);
71
+ if (searchParams.get(PROJECT_MOUNT_QUERY) !== instanceId) return [];
72
+ const at = pathname.lastIndexOf('/src/');
73
+ return at === -1 ? [] : [pathname.slice(at + 1)];
74
+ });
75
+ return [...new Set(paths)].sort();
76
+ }
77
+
78
+ /**
79
+ * The `modules` binding of `vgai eval`'s `game.run` scope and of the
80
+ * `game-eval` op — a RESOLVER, `modules('src/sim/host.ts')`, that also
81
+ * ENUMERATES what it can resolve.
82
+ *
83
+ * WHY THE ENUMERATION EXISTS (measured, cold mug #2, 2026-08-29). There is no
84
+ * module registry here and there never was: `modules` is a function. So a cold
85
+ * agent's first move — `game.run(({ modules }) => Object.keys(modules))` —
86
+ * answered `[]` against a mount with twelve loaded modules, and `[]` reads as
87
+ * "this game registers none". A function whose only honest answer to
88
+ * `Object.keys` is an empty array is a door that lies when asked what is
89
+ * behind it. `loaded` is an enumerable own accessor, so `Object.keys(modules)`
90
+ * now names it and `modules.loaded` is the list of paths to pass back in.
91
+ *
92
+ * WHY THE RESULT IS A GUARDED PROMISE (measured, cold fox #3, 2026-08-29).
93
+ * `modules(path)` is ASYNC — it dynamic-imports the running mount's own url —
94
+ * and a cold agent who skipped the `await` got
95
+ * `TypeError: modules(...).liveFoxes is not a function`, which names the
96
+ * export, names the module, and says nothing whatsoever about promises. The
97
+ * misuse is exactly detectable, so it is now caught where it happens: reading
98
+ * any non-Promise member off the returned value throws a sentence naming the
99
+ * fix. That is the same treatment `loaded` got — a door that answers the
100
+ * question actually being asked instead of a true-but-useless fact.
101
+ */
102
+ export function createGameModuleResolver(
103
+ instanceId?: string,
104
+ ): ((path: string) => Promise<Record<string, unknown>>) & { readonly loaded: string[] } {
105
+ const resolve = (path: string) => awaitOnly(path, importGameModule(path, instanceId));
106
+ Object.defineProperty(resolve, 'loaded', {
107
+ enumerable: true,
108
+ get: () => {
109
+ const id = instanceId ?? focusedInstanceProvider?.();
110
+ return id ? loadedGameModulePaths(id) : [];
111
+ },
112
+ });
113
+ return resolve as ((path: string) => Promise<Record<string, unknown>>) & {
114
+ readonly loaded: string[];
115
+ };
116
+ }
117
+
118
+ /** Everything a legitimate consumer of a promise reads off it. Anything else
119
+ * is a caller that forgot the `await` and is reaching for a module export. */
120
+ const PROMISE_MEMBERS = new Set(['then', 'catch', 'finally', 'constructor', 'toJSON']);
121
+
122
+ /**
123
+ * The promise `modules(path)` hands back, with one behaviour added: reading a
124
+ * member that is not a promise's own throws a sentence naming the `await`.
125
+ *
126
+ * `then`/`catch`/`finally` are forwarded BOUND to the real promise — a
127
+ * Proxy has no promise internal slots, so `Promise.prototype.then` called with
128
+ * the proxy as `this` would throw for the wrong reason and break every correct
129
+ * caller.
130
+ */
131
+ function awaitOnly(
132
+ path: string,
133
+ promise: Promise<Record<string, unknown>>,
134
+ ): Promise<Record<string, unknown>> {
135
+ return new Proxy(promise, {
136
+ get(target, key) {
137
+ if (key === 'then' || key === 'catch' || key === 'finally') {
138
+ return (Reflect.get(target, key) as (...args: unknown[]) => unknown).bind(target);
139
+ }
140
+ if (typeof key === 'symbol' || PROMISE_MEMBERS.has(key)) return Reflect.get(target, key);
141
+ throw new Error(
142
+ `modules("${path}") returns a PROMISE — it dynamic-imports the running mount's own copy ` +
143
+ `of that module — so \`.${String(key)}\` reads a promise, not your export. Await it: ` +
144
+ `\`const m = await modules("${path}"); m.${String(key)}\` (and make the \`game.run\` ` +
145
+ 'callback `async`).',
146
+ );
147
+ },
148
+ });
149
+ }
150
+
151
+ /** The running mount's instance of one project module. */
152
+ export async function importGameModule(
153
+ path: string,
154
+ instanceId?: string,
155
+ ): Promise<Record<string, unknown>> {
156
+ const clean = normalizeGameModulePath(path);
157
+ const id = instanceId ?? focusedInstanceProvider?.();
158
+ if (!id) {
159
+ throw new Error(`modules(): no play instance is mounted — enter Play first (${commandLine('play')}).`);
160
+ }
161
+ // Resolve the served base from what the browser ACTUALLY LOADED for this
162
+ // mount, never from a rebuilt path: Vite rewrites a mount's imports to its
163
+ // own resolved forms (root-relative in-repo, /@fs in a scaffold), and any
164
+ // byte of divergence yields a SECOND module instance — the phantom this
165
+ // lane exists to make impossible. Measured before this guard: an /@fs
166
+ // rebuild read a dead copy while the registered door watched the sweep run.
167
+ // ZERO URL CONSTRUCTION. Every attempt to rebuild the served url from
168
+ // parts produced a phantom second instance off by one byte (/@fs vs
169
+ // root-relative; a Vite-appended ?import). The only string guaranteed to
170
+ // name the RUNNING instance is the one the graph already fetched — so the
171
+ // accessor imports that string VERBATIM from its owning realm's evaluated
172
+ // module URLs, and refuses when the module has not been
173
+ // loaded by the game yet. The running graph's modules are, by definition,
174
+ // loaded — so this covers exactly what "touch the running game" means.
175
+ const suffix = `/${clean}`;
176
+ const url = moduleUrls(id).find((name) => {
177
+ const u = new URL(name);
178
+ return u.pathname.endsWith(suffix) && u.searchParams.get(PROJECT_MOUNT_QUERY) === id;
179
+ });
180
+ if (!url) {
181
+ const candidates = loadedGameModulePaths(id);
182
+ throw new Error(
183
+ `modules("${path}"): the running mount has not loaded that module (nothing to reach — a ` +
184
+ 'module the game never imported has no live instance). Loaded project modules ' +
185
+ `(also readable as modules.loaded): ${candidates.join(', ') || '(none yet — play first)'}`,
186
+ );
187
+ }
188
+ try {
189
+ return (await import(/* @vite-ignore */ url)) as Record<string, unknown>;
190
+ } catch (error) {
191
+ throw new Error(
192
+ `modules("${path}") could not load ${url} — the path must be the exact served file ` +
193
+ `(check the extension: .ts vs .tsx). ${error instanceof Error ? error.message : String(error)}`,
194
+ );
195
+ }
196
+ }