@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,358 @@
1
+ /**
2
+ * THE GAME'S PAGE, IN A BOX — the host-side half of "an ingested game runs
3
+ * in-realm, but it does not own the tab".
4
+ *
5
+ * `gated-globals.ts` already pipes a SCOPED `window`/`document` into game
6
+ * modules so their input listeners can be gated. This module is the same idea
7
+ * carried to everything else a page-owning game reaches for, because a game
8
+ * written to own the whole browser window says so in four ways that all break
9
+ * the editor when it runs inside a pane:
10
+ *
11
+ * 1. `document.body.appendChild(canvas)` — the game's canvas, title screen,
12
+ * pause overlay and game-over screen all land at PAGE level, painting over
13
+ * editor chrome (such a title wrapper is typically `position:fixed` with a
14
+ * z-index in the hundreds). Substituting the mount's adopted
15
+ * surface for `document.body` puts every one of them inside the pane
16
+ * instead — including the ones the host never sees, because it is the
17
+ * game's OWN append that is redirected, not a post-hoc reparent of the
18
+ * things we thought to look for. It is also what makes teardown total:
19
+ * removing the surface removes the game's whole DOM, overlays included.
20
+ * 2. `position:fixed` inside that surface would STILL escape to the viewport,
21
+ * so the surface declares {@link GAME_SURFACE_CONTAINMENT_CSS} — `contain:
22
+ * layout` makes it the containing block for fixed and absolute
23
+ * descendants, so a game's full-screen overlay is full-PANE instead. This
24
+ * is a host policy, not per-game CSS: every ingested game with page-level
25
+ * overlays gets it.
26
+ * 3. `window.innerWidth`/`innerHeight` + a `resize` listener is how a game
27
+ * computes its own letterbox, in its `onResize` handler and again at
28
+ * init. Reporting the real window to
29
+ * a game living in a pane is a lie that shows up as a stretched or
30
+ * cropped picture, so the realm reports the SURFACE's size and the host
31
+ * dispatches `resize` to the realm's own listeners when the pane changes.
32
+ * A game that letterboxes itself then letterboxes to the pane, with no
33
+ * host aspect policy to get wrong.
34
+ * 4. `window.location.href = …` navigates the EDITOR away (a game menu with a
35
+ * "quit" link does exactly this). See
36
+ * `game-location-guard.ts`.
37
+ *
38
+ * ONE page per game realm, registered by the mount that owns the surface and
39
+ * dropped when it disposes. Nothing here is per-game: the mount registers a
40
+ * surface, the realm reports it, and a game that never touches `document.body`
41
+ * or `window.innerWidth` cannot tell the difference.
42
+ */
43
+
44
+ /**
45
+ * What the host stamps on an adopted surface so a game's `position:fixed`
46
+ * overlay is contained to the pane.
47
+ *
48
+ * `contain: layout` makes the element a containing block for absolutely AND
49
+ * fixed positioned descendants — the one CSS primitive that does this without
50
+ * the side effects of the alternatives (`transform`/`filter` also create the
51
+ * containing block but force a compositing layer and, on the ADOPTED element
52
+ * itself, are exactly the stamp {@link claimHostSurfaceBox} exists to clear).
53
+ */
54
+ export const GAME_SURFACE_CONTAINMENT_CSS = 'contain: layout;';
55
+
56
+ /** `EventTarget`'s own listener identity is (type, callback, capture) — read
57
+ * the capture flag out of either options form so the page's registration
58
+ * bookkeeping dedupes exactly the way the bus does. */
59
+ function captureOf(options?: boolean | AddEventListenerOptions | EventListenerOptions): boolean {
60
+ return typeof options === 'boolean' ? options : (options?.capture ?? false);
61
+ }
62
+
63
+ /** Mark carrying the text this observer last wrote, so its own write is not
64
+ * re-scoped and a game's next rewrite is. */
65
+ export const SCOPED_STYLE_MARK = 'vgaiScopedCss';
66
+
67
+ /**
68
+ * The `@scope` rewriter, loaded on FIRST USE — the one page-side door to the
69
+ * CSS transform. The transform itself is a pure postcss function that happens
70
+ * to live under `server/` because the dev server's `/__editor/scoped-game-css`
71
+ * route is its other caller; the lazy import (and the reason it is lazy) is
72
+ * {@link observeInjectedStyles}'s note below, and every page-side caller shares
73
+ * this one memo so a realm that scopes both boot-time inline CSS and runtime
74
+ * injected `<style>`s parses the loader once.
75
+ *
76
+ * Named here rather than re-derived per caller because the `@editor/` alias is
77
+ * the only door a PACKAGE has into the host: the ingest lane's
78
+ * `served-html-boot.ts` scopes a served document's inline CSS through exactly
79
+ * this function, and a package reaching `packages/editor/server/` directly is
80
+ * not a seam that exists.
81
+ */
82
+ let gameCssScoper: Promise<(css: string) => string> | null = null;
83
+ export function loadGameCssScoper(): Promise<(css: string) => string> {
84
+ gameCssScoper ??= Promise.all([
85
+ import('@volter/editor-core/game-css-scope-transform'),
86
+ import('@volter/editor-sdk/session/game-css-scope'),
87
+ ]).then(
88
+ ([{ scopeGameCss }, { GAME_CSS_SCOPE_SELECTOR }]) =>
89
+ (css: string) =>
90
+ scopeGameCss({ css, scopeSelector: GAME_CSS_SCOPE_SELECTOR, styleSheetPath: '/' }),
91
+ );
92
+ return gameCssScoper;
93
+ }
94
+
95
+ /**
96
+ * CSS applies document-wide wherever its `<style>` sits, so a style element a
97
+ * game creates at RUNTIME — a CDN utility framework's live sheet, a game's own
98
+ * theme injection — would restyle the editor chrome from inside the realm's
99
+ * head div. This observer runs every added or rewritten `<style>` under the
100
+ * surface through the SAME `@scope` transform declared sheets and boot-time
101
+ * inline CSS get. The heavy transform (postcss) loads on first use; until it
102
+ * does, the raw text is cleared rather than left leaking — a game's sheet
103
+ * arriving a beat late is honest, the editor repainted by it is not.
104
+ *
105
+ * ## "First use" means the first STYLE, not the first surface
106
+ *
107
+ * The import below is deliberately started by {@link scopeStyle}, not by this
108
+ * function. Attaching a surface is not evidence that anything will ever inject
109
+ * a `<style>`, and the overwhelming majority of games never do — a first-party
110
+ * R3F/Pixi world paints into a canvas and touches no page CSS at all.
111
+ *
112
+ * Loading it on ATTACH made every Play in every project pay for a CSS parser
113
+ * it would not use, and the cost was not just bytes: `postcss` destructures
114
+ * `fs`/`path`/`url`/`source-map-js` at module scope, and Vite honours postcss's
115
+ * own `browser` field by substituting a proxy that console.warns per property
116
+ * read. So a bare scaffolded project pressing Play emitted THIRTEEN
117
+ * "Module … has been externalized for browser compatibility" warnings —
118
+ * `fs.existsSync/readFileSync/realpathSync`,
119
+ * `path.isAbsolute/resolve/dirname/join/relative/sep`,
120
+ * `url.fileURLToPath/pathToFileURL`,
121
+ * `source-map-js.SourceMapConsumer/SourceMapGenerator` — straight into the
122
+ * game realm's console, which made the console-silence contract unreachable in
123
+ * a fresh scaffold and every `vgai` verb exit non-zero (measured three times:
124
+ * the donut, cold-barrel and lit-lanes probes). The chain was
125
+ * `play-mode.enterPlayModeInner` -> `gated-globals.setGameSurface` ->
126
+ * `GameRealmPage.surface` -> `observeInjectedStyles` -> `import('../server/
127
+ * scoped-game-css')` -> `postcss`. A game that genuinely injects a runtime
128
+ * `<style>` still loads it, because there the parser is the feature working.
129
+ */
130
+ function observeInjectedStyles(surface: HTMLElement): MutationObserver {
131
+ let scoper: ((css: string) => string) | null = null;
132
+ const loadScoper = (): Promise<void> =>
133
+ loadGameCssScoper().then((loaded) => {
134
+ scoper = loaded;
135
+ });
136
+ const scopeStyle = (style: HTMLStyleElement): void => {
137
+ const text = style.textContent ?? '';
138
+ if (text.length === 0 || style.dataset[SCOPED_STYLE_MARK] === text) return;
139
+ if (!scoper) {
140
+ const raw = text;
141
+ style.textContent = '';
142
+ void loadScoper().then(() => {
143
+ if (style.isConnected && (style.textContent ?? '') === '') {
144
+ style.textContent = raw;
145
+ scopeStyle(style);
146
+ }
147
+ });
148
+ return;
149
+ }
150
+ let scoped: string;
151
+ try {
152
+ scoped = scoper(text);
153
+ } catch {
154
+ style.textContent = ''; // unscopeable CSS stays uninstalled — never raw
155
+ return;
156
+ }
157
+ style.dataset[SCOPED_STYLE_MARK] = scoped;
158
+ style.textContent = scoped;
159
+ };
160
+ const styleOf = (node: Node): HTMLStyleElement | null => {
161
+ if (node instanceof HTMLStyleElement) return node;
162
+ const parent = node.parentElement;
163
+ return parent instanceof HTMLStyleElement ? parent : null;
164
+ };
165
+ const observer = new MutationObserver((records) => {
166
+ const seen = new Set<HTMLStyleElement>();
167
+ for (const record of records) {
168
+ if (record.type === 'childList') {
169
+ for (const node of record.addedNodes) {
170
+ const direct = styleOf(node);
171
+ if (direct) seen.add(direct);
172
+ else if (node instanceof Element) {
173
+ for (const style of node.querySelectorAll('style')) seen.add(style);
174
+ }
175
+ }
176
+ } else if (record.type === 'characterData') {
177
+ const style = styleOf(record.target);
178
+ if (style) seen.add(style);
179
+ }
180
+ }
181
+ for (const style of seen) if (style.isConnected) scopeStyle(style);
182
+ });
183
+ observer.observe(surface, { childList: true, subtree: true, characterData: true });
184
+ for (const style of surface.querySelectorAll('style')) scopeStyle(style);
185
+ return observer;
186
+ }
187
+
188
+ /**
189
+ * The page one game realm sees. Holds the surface element, the realm-scoped
190
+ * `resize` bus, and a live count of the game's own resize listeners — that
191
+ * count is the predicate the mount reads to decide whether the GAME resizes
192
+ * itself or the HOST resizes it (see `ingest-root-adapter.ts`'s `resize`).
193
+ */
194
+ export class GameRealmPage {
195
+ /** The element this realm sees as `document.body`. */
196
+ private surfaceElement: HTMLElement | null = null;
197
+ /** Scopes every `<style>` the game injects at runtime — see {@link attachSurface}. */
198
+ private injectedStyles: MutationObserver | null = null;
199
+
200
+ get surface(): HTMLElement | null {
201
+ return this.surfaceElement;
202
+ }
203
+
204
+ set surface(el: HTMLElement | null) {
205
+ this.injectedStyles?.disconnect();
206
+ this.injectedStyles = null;
207
+ this.surfaceElement = el;
208
+ if (el) this.injectedStyles = observeInjectedStyles(el);
209
+ }
210
+
211
+ /** Page metadata is realm state, not permission to rename the editor tab. */
212
+ private pageTitle = '';
213
+
214
+ /**
215
+ * The classic script the host is evaluating for this page.
216
+ *
217
+ * A served bundle still runs through the realm-shadowing prelude rather than
218
+ * directly in the editor tab. That means the browser cannot populate
219
+ * `document.currentScript` for it, even though webpack's automatic public
220
+ * path runtime requires that ordinary classic-script signal. The served
221
+ * document loader carries the declared entry here for exactly the duration
222
+ * of evaluation, and the document proxy reports it below.
223
+ */
224
+ private executingClassicScript: HTMLScriptElement | null = null;
225
+
226
+ /** A connected, surface-owned destination for page-level style/script/link
227
+ * inserts. It intentionally is not the editor's real `document.head`. */
228
+ private headElement: HTMLElement | null = null;
229
+
230
+ /** Where the realm's own `window.addEventListener('resize', …)` lands.
231
+ * Replaced wholesale by {@link reset} — `EventTarget` has no "remove them
232
+ * all", and a dead game's handlers must not survive into the next mount. */
233
+ private resizeBus = new EventTarget();
234
+
235
+ /**
236
+ * The game's live `resize` registrations, keyed exactly the way `EventTarget`
237
+ * itself keys them: (listener, capture). A plain counter was WRONG in both
238
+ * directions — a duplicate `addEventListener` with the same pair bumped it
239
+ * while the bus deduped, and a `removeEventListener` that matched nothing
240
+ * decremented it anyway — and the count is not bookkeeping: it IS the
241
+ * predicate {@link gameResizesItself} the mount reads to decide whether the
242
+ * host may size the game.
243
+ */
244
+ private readonly resizeListeners = new Map<EventListenerOrEventListenerObject, Set<boolean>>();
245
+
246
+ addResizeListener(
247
+ listener: EventListenerOrEventListenerObject,
248
+ options?: boolean | AddEventListenerOptions,
249
+ ): void {
250
+ let captures = this.resizeListeners.get(listener);
251
+ if (!captures) {
252
+ captures = new Set();
253
+ this.resizeListeners.set(listener, captures);
254
+ }
255
+ captures.add(captureOf(options));
256
+ this.resizeBus.addEventListener('resize', listener, options);
257
+ }
258
+
259
+ removeResizeListener(
260
+ listener: EventListenerOrEventListenerObject,
261
+ options?: boolean | EventListenerOptions,
262
+ ): void {
263
+ const captures = this.resizeListeners.get(listener);
264
+ if (captures) {
265
+ captures.delete(captureOf(options));
266
+ if (captures.size === 0) this.resizeListeners.delete(listener);
267
+ }
268
+ this.resizeBus.removeEventListener('resize', listener, options);
269
+ }
270
+
271
+ /**
272
+ * True when the game registered at least one `resize` listener of its own —
273
+ * i.e. it has a size policy and the host must not overwrite it.
274
+ */
275
+ get gameResizesItself(): boolean {
276
+ return this.resizeListeners.size > 0;
277
+ }
278
+
279
+ /**
280
+ * Return the page to "no game is running".
281
+ *
282
+ * A page OUTLIVES the mount that registered its surface — it is keyed by
283
+ * realm id in `gated-globals.ts`'s `pages` map, and the realm's proxies close
284
+ * over the object, so it cannot be dropped and replaced. Clearing only the
285
+ * surface therefore leaked a dead game's size POLICY into the next mount in
286
+ * the same tab: the second ingest inherited `gameResizesItself === true` from
287
+ * a game that no longer exists, and the host stopped sizing it. The listeners
288
+ * on the bus leaked with it.
289
+ */
290
+ reset(): void {
291
+ this.headElement?.remove();
292
+ this.headElement = null;
293
+ this.pageTitle = '';
294
+ this.executingClassicScript = null;
295
+ this.surface = null;
296
+ this.resizeListeners.clear();
297
+ this.resizeBus = new EventTarget();
298
+ }
299
+
300
+ /** Tell the game its window changed size. Called by the mount's pane
301
+ * ResizeObserver; a no-op for a game that registered nothing. */
302
+ dispatchResize(): void {
303
+ this.resizeBus.dispatchEvent(new Event('resize'));
304
+ }
305
+
306
+ /** The realm's `window.innerWidth`/`innerHeight`, or `null` before a surface
307
+ * is registered (then the real window's values stand). */
308
+ viewport(): { width: number; height: number } | null {
309
+ const el = this.surface;
310
+ if (!el) return null;
311
+ return { width: el.clientWidth, height: el.clientHeight };
312
+ }
313
+
314
+ get title(): string {
315
+ return this.pageTitle;
316
+ }
317
+
318
+ set title(value: string) {
319
+ this.pageTitle = String(value);
320
+ }
321
+
322
+ /** What this realm sees as `document.currentScript`. */
323
+ get currentScript(): HTMLScriptElement | null {
324
+ return this.executingClassicScript;
325
+ }
326
+
327
+ /**
328
+ * Evaluate one host-loaded classic entry with the same `currentScript.src`
329
+ * its real `<script src>` would have supplied. Restoration is stack-safe so
330
+ * a nested loader cannot leak its carrier into its caller.
331
+ */
332
+ withCurrentScript<T>(src: string, run: () => T): T {
333
+ const previous = this.executingClassicScript;
334
+ const script = document.createElement('script');
335
+ script.src = src;
336
+ this.executingClassicScript = script;
337
+ try {
338
+ return run();
339
+ } finally {
340
+ this.executingClassicScript = previous;
341
+ }
342
+ }
343
+
344
+ /** The game's page head, contained by its mount. Created lazily so a game
345
+ * that never uses page-level inserts adds no DOM of its own. */
346
+ head(): HTMLElement | null {
347
+ const surface = this.surface;
348
+ if (!surface) return null;
349
+ if (!this.headElement || this.headElement.parentElement !== surface) {
350
+ const head = document.createElement('div');
351
+ head.dataset['vgaiGameHead'] = '';
352
+ head.hidden = true;
353
+ surface.prepend(head);
354
+ this.headElement = head;
355
+ }
356
+ return this.headElement;
357
+ }
358
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The ONE way a mounted game's browser-resource realm is ended.
3
+ *
4
+ * Every surface that runs game code in this tab — first-party play, an extra
5
+ * play instance, and each of the three ingest teardowns — owns a
6
+ * `gated-globals.ts` realm: the window/document proxies its listeners
7
+ * registered through, its timer/frame gate, and its page. Ending a mount and
8
+ * NOT ending its realm is invisible (nothing throws, nothing renders wrong) and
9
+ * permanent: the listeners stay attached to the real `window`, and the next
10
+ * mount under the same id inherits them.
11
+ *
12
+ * Extracted so play mode and ingest cannot drift — ingest used to reset the
13
+ * input gate to always-true and stop there, which left a dead game's listeners
14
+ * attached AND ungated.
15
+ */
16
+
17
+ import { editorConsole } from '@volter/editor-core/editor-console';
18
+ import { disposeGameRealm } from './gated-globals';
19
+
20
+ /**
21
+ * End one mount's realm and report anything the game left behind.
22
+ *
23
+ * `id` is the mount id; `''` (the default) is the DEFAULT realm — what ingest
24
+ * and every module served without a mount id resolve through. `name` is what
25
+ * the leak report calls the stopped game.
26
+ *
27
+ * The report is the point: a game that released everything it registered is
28
+ * silent, and a game that did not is named, with the count, on the editor
29
+ * console (and therefore in `vgai console`). The editor reclaims either way.
30
+ */
31
+ export function reclaimGameRealm(id: string, name: string): void {
32
+ const released = disposeGameRealm(id);
33
+ const timerCount =
34
+ released.timers.timeouts +
35
+ released.timers.intervals +
36
+ released.timers.animationFrames +
37
+ released.timers.parkedCallbacks;
38
+ if (released.listeners === 0 && timerCount === 0) return;
39
+ const parts: string[] = [];
40
+ if (released.listeners > 0) {
41
+ parts.push(
42
+ `${released.listeners} window/document listener${released.listeners === 1 ? '' : 's'}`,
43
+ );
44
+ }
45
+ if (timerCount > 0) {
46
+ parts.push(`${timerCount} timer/frame callback${timerCount === 1 ? '' : 's'}`);
47
+ }
48
+ editorConsole.logStructured(
49
+ 'warn',
50
+ `${name} stopped without releasing ${parts.join(' and ')}; the editor reclaimed them.`,
51
+ 'environment',
52
+ undefined,
53
+ { instanceId: id, instanceName: name },
54
+ );
55
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A game-realm view of ordinary same-origin browser storage.
3
+ *
4
+ * Storage DATA stays shared, exactly as it is for two tabs on one origin. The
5
+ * facade exists for a different reason: project code must not receive the
6
+ * editor's actual Storage instance and whose calls otherwise cannot be
7
+ * attributed to the mount that made them. A transient mount id is not a
8
+ * durable profile id, so this deliberately does not prefix, copy, or
9
+ * virtualize keys.
10
+ */
11
+
12
+ export interface GameRealmStorageStats {
13
+ readonly reads: number;
14
+ readonly writes: number;
15
+ }
16
+
17
+ export interface GameRealmStorageView {
18
+ readonly storage: Storage;
19
+ stats(): GameRealmStorageStats;
20
+ }
21
+
22
+ export function createGameRealmStorage(source: Storage): GameRealmStorageView {
23
+ let reads = 0;
24
+ let writes = 0;
25
+ const target = Object.create(Object.getPrototypeOf(source)) as object;
26
+
27
+ const getItem = (key: string): string | null => {
28
+ reads += 1;
29
+ return source.getItem(String(key));
30
+ };
31
+ const setItem = (key: string, value: string): void => {
32
+ writes += 1;
33
+ source.setItem(String(key), String(value));
34
+ };
35
+ const removeItem = (key: string): void => {
36
+ writes += 1;
37
+ source.removeItem(String(key));
38
+ };
39
+ const clear = (): void => {
40
+ writes += 1;
41
+ source.clear();
42
+ };
43
+ const key = (index: number): string | null => {
44
+ reads += 1;
45
+ return source.key(Number(index));
46
+ };
47
+
48
+ const facade = new Proxy(target, {
49
+ get(_target, property) {
50
+ if (property === 'length') {
51
+ reads += 1;
52
+ return source.length;
53
+ }
54
+ if (property === 'getItem') return getItem;
55
+ if (property === 'setItem') return setItem;
56
+ if (property === 'removeItem') return removeItem;
57
+ if (property === 'clear') return clear;
58
+ if (property === 'key') return key;
59
+ if (property === Symbol.toStringTag) return 'Storage';
60
+ if (typeof property === 'string') return getItem(property) ?? undefined;
61
+ return Reflect.get(target, property, target);
62
+ },
63
+ set(_target, property, value) {
64
+ if (typeof property !== 'string') return Reflect.set(target, property, value, target);
65
+ setItem(property, String(value));
66
+ return true;
67
+ },
68
+ deleteProperty(_target, property) {
69
+ if (typeof property !== 'string') return Reflect.deleteProperty(target, property);
70
+ removeItem(property);
71
+ return true;
72
+ },
73
+ defineProperty(_target, property, descriptor) {
74
+ if (typeof property !== 'string' || !('value' in descriptor)) return false;
75
+ setItem(property, String(descriptor.value));
76
+ return true;
77
+ },
78
+ has(_target, property) {
79
+ if (typeof property !== 'string') return Reflect.has(target, property);
80
+ if (property in target) return true;
81
+ return getItem(property) !== null;
82
+ },
83
+ ownKeys() {
84
+ const keys: string[] = [];
85
+ for (let index = 0; index < source.length; index += 1) {
86
+ const name = source.key(index);
87
+ if (name !== null) keys.push(name);
88
+ }
89
+ return keys;
90
+ },
91
+ getOwnPropertyDescriptor(_target, property) {
92
+ if (typeof property !== 'string') return undefined;
93
+ const value = source.getItem(property);
94
+ return value === null
95
+ ? undefined
96
+ : { configurable: true, enumerable: true, value, writable: true };
97
+ },
98
+ }) as Storage;
99
+
100
+ return {
101
+ storage: facade,
102
+ stats: () => ({ reads, writes }),
103
+ };
104
+ }