@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,316 @@
1
+ /**
2
+ * `bridge-screenshot` and the recording verbs' shared frame plumbing, moved
3
+ * out of the editor's `command-listener.ts` with the verbs they serve
4
+ * (WORK.md §The workbench). Play mode, the play/gameplay recorders, the
5
+ * composite capture and the editor console are reached through the
6
+ * `@editor/*` alias until those modules move too. Nothing here is host API.
7
+ */
8
+
9
+ import { commandLine } from '@volter/editor-core/product-command';
10
+ import {
11
+ type CaptureFlatness,
12
+ CaptureLayerError,
13
+ capturePlayComposite,
14
+ sampleFlatness,
15
+ } from '@volter/editor-core/composite-screenshot';
16
+ import { editorConsole } from '@volter/editor-core/editor-console';
17
+ import { presentationSurface } from '@volter/editor-core/presentation-surface';
18
+ import type { EditorCommandMessage, EditorCommandResult } from '@volter/editor-sdk/commands';
19
+ import { notPlayingResult, structuredErrorResult } from '../command-results';
20
+ import { isIngestActive } from '../ingest/active-ingest';
21
+ import {
22
+ getInstanceCanvas,
23
+ getInstanceContainer,
24
+ getPlayRuntimeAccess,
25
+ isPlayModeActive,
26
+ } from '../play/play-mode';
27
+ import { livePlayRecording } from '../play/play-recording';
28
+ import { hasLiveDebugPlane } from './dispatch';
29
+ import { captureLiveCanvasFrame } from './live-frames';
30
+
31
+ /** `bridge-screenshot` relay op handler — captures the live play-mode game
32
+ * view and returns it in the same `{base64, mimeType}` shape
33
+ * `capture-viewport` already uses. #146: the capture is now the FULL game
34
+ * stack — canvas(es) plus the DOM UI layers (React adapter roots,
35
+ * react-world layers) composited via `capturePlayComposite` — because a
36
+ * canvas-only frame of a game whose score/health lives in the HUD is not
37
+ * "what the game looks like". When the composite leg can't run (no
38
+ * container, no DOM Image/XMLSerializer — e.g. the node-side relay tests —
39
+ * or a rasterization failure), it degrades to the original canvas-only
40
+ * `toDataURL` capture, marked
41
+ * honestly with `composite: false`. A caller (`RelayTransport.screenshot`
42
+ * in `@volter/editor-live`) writes the PNG to disk — this relay op stays a pure
43
+ * "hand back the pixels" primitive, matching `bridge-call`'s own
44
+ * session-generic, run-lifecycle-free shape.
45
+ *
46
+ * Both legs also carry the PIXEL-honesty fields — `flatness` (how much of the
47
+ * frame is one flat surface, with the caller-facing sentence written here
48
+ * where the pixels are) and `loopRecoveryFrame` (this frame exists because
49
+ * capture recovered a starved runtime with one deterministic tick). Shaped by
50
+ * {@link screenshotResult} so the two legs cannot drift apart.
51
+ *
52
+ * BOTH legs read a canvas, and a canvas is only readable after its frame when
53
+ * its WebGL context was created with `preserveDrawingBuffer: true`. Every
54
+ * canvas the vgai RUNTIME mounts sets it (`@volter/game-runtime/runtime/create-runtime`,
55
+ * D5 §1/§4); a canvas an INGESTED game created is the game's own and usually
56
+ * does not, which read as a silently BLACK screenshot over a perfectly
57
+ * healthy running game. {@link liveCanvasFrame} is the answer for that
58
+ * case — one seam over both substrates' recovery mechanisms — and it
59
+ * is `null` for everything else, leaving the first-party path untouched. */
60
+
61
+ /** The clip a delivered still belongs to — see {@link screenshotRecordingNotice}. */
62
+ interface GameCaptureRecordingNote {
63
+ readonly path: string;
64
+ readonly startedAt: string;
65
+ /** The sentence the caller shows verbatim, written here where the pixels are. */
66
+ readonly notice: string;
67
+ }
68
+
69
+ function screenshotResult(
70
+ base64: string,
71
+ parts: {
72
+ composite: boolean;
73
+ layers?: { canvases: number; domOverlays: number };
74
+ flatness: CaptureFlatness | null | undefined;
75
+ loopRecoveryFrame: boolean;
76
+ },
77
+ ): EditorCommandResult {
78
+ const recording = screenshotRecordingNotice();
79
+ return {
80
+ ok: true,
81
+ data: {
82
+ base64,
83
+ mimeType: 'image/png',
84
+ composite: parts.composite,
85
+ ...(parts.layers ? { layers: parts.layers } : {}),
86
+ ...(parts.flatness ? { flatness: parts.flatness } : {}),
87
+ ...(parts.loopRecoveryFrame ? { loopRecoveryFrame: true } : {}),
88
+ ...(recording ? { recording } : {}),
89
+ },
90
+ };
91
+ }
92
+
93
+ /**
94
+ * WHAT A STILL CAN AND CANNOT ANSWER, said ON the still.
95
+ *
96
+ * A screenshot answers "what does it look like". It cannot answer "did the
97
+ * jump land", "did the enemy path around the wall", "did the shot register" —
98
+ * a single frame answers those only by luck, and a lucky answer is
99
+ * indistinguishable from a real one, which is how a broken game gets reported
100
+ * as working. So every capture taken during a recorded run carries the clip's
101
+ * coordinates: the file, the wall-clock start its offsets are measured from,
102
+ * and the play log whose event timestamps convert an event into an offset.
103
+ *
104
+ * THIS USED TO BE A REFUSAL, AND THE REFUSAL WAS THE DEFECT (2026-08-29,
105
+ * cold fox #3). Because `vgai play` always records, refusing the still made
106
+ * `vgai screenshot` dead for the rest of the session — and the look lane's
107
+ * questions ("is the ear proportion right", "is the fox sunk into the
108
+ * ground") are exactly the ones a still DOES answer. The refusal was
109
+ * discouraging a wrong question by making a right one impossible, and the
110
+ * cold agents' whole loop is play → look.
111
+ *
112
+ * Nothing about the clip changed: play still records unconditionally
113
+ * (`play-recording.ts` — the evidence must not depend on anyone remembering
114
+ * to ask for it), and the capture reads the canvas and composites the DOM
115
+ * layers into an offscreen image, which the recorder's own stream never sees.
116
+ * What changed is that the redirect is now attached to a delivered frame
117
+ * instead of replacing it.
118
+ *
119
+ * No tool instructions here on purpose: the reader knows how to extract frames
120
+ * from a WebM. What they cannot know without being told is WHERE the clip is,
121
+ * WHEN it started, and HOW MANY frames constitute looking.
122
+ */
123
+ function screenshotRecordingNotice(): GameCaptureRecordingNote | null {
124
+ const live = livePlayRecording();
125
+ if (!live) return null;
126
+ return {
127
+ path: live.path,
128
+ startedAt: live.startedAt,
129
+ notice: [
130
+ `This run is being RECORDED — the still above is one frame of ${live.path}.`,
131
+ 'A still cannot answer a question about a moving game (did the jump land, did the shot',
132
+ 'register). If that is the question, read the clip:',
133
+ '',
134
+ ` recording: ${live.path}${live.rotates ? ` (the next ${commandLine('play')} replaces this file)` : ' (named — never rotated)'}`,
135
+ ...(live.format === 'canvas-dom'
136
+ ? [
137
+ ` HUD replay: ${live.replayPath}`,
138
+ ' The raw WebM contains the canvas only. Reconstruct HUD stills with',
139
+ ' editor.recording.captureReplay, or export a full video with editor.recording.exportReplay.',
140
+ ]
141
+ : []),
142
+ ` startedAt: ${live.startedAt} (clip offset 0)`,
143
+ ` play log: ${live.logFile === null ? '(none — nothing is logging this run)' : `logs/${live.logFile}`} (event timestamps → clip offsets)`,
144
+ '',
145
+ 'THE BAR for a temporal question: examine AT LEAST 10 frames spanning no more than 5',
146
+ 'seconds around the moment. One frame, or ten frames spread across the run, is the same',
147
+ 'guess a single screenshot was.',
148
+ '',
149
+ `The clip is finalized and readable when play stops — ${commandLine('stop')}, or the idle`,
150
+ `auto-stop after ${Math.round(live.idleAutoStopMs / 1000)}s with no session command and no player input.`,
151
+ ].join('\n'),
152
+ };
153
+ }
154
+
155
+ export async function handleBridgeScreenshot(
156
+ cmd: EditorCommandMessage,
157
+ ): Promise<EditorCommandResult> {
158
+ // A recorded run does NOT gate this capture — `screenshotResult` attaches the
159
+ // clip's coordinates to the frame instead (see `screenshotRecordingNotice`).
160
+ // Same reachability rule `handleBridgeCall` uses, and for the same reason:
161
+ // an ingested game's mount IS the running game, so it never creates a
162
+ // first-party play session and `isPlayModeActive()` alone made
163
+ // `vgai screenshot` permanently unusable against ingest. Loosening the gate
164
+ // costs nothing in honesty — the ingest mount renders into the very
165
+ // container `getInstanceContainer()` returns (`play-mode.ts`'s
166
+ // `getGameContainer` is shared with the ingest mount paths), and when there is
167
+ // genuinely nothing to capture both legs below still refuse by name with
168
+ // `BRIDGE_SCREENSHOT_UNAVAILABLE`.
169
+ //
170
+ // `isIngestActive()` is the third reader, and it is what makes the refusal
171
+ // TRUE. `hasLiveDebugPlane()` covers an ingest that publishes a debug plane
172
+ // projected from a declared contract — which a canvas ingest has no way to
173
+ // do (`mount-canvas-ingest-root.ts` publishes `setActiveSystems({})`), so
174
+ // `vgai screenshot` answered "not in play mode" over a mounted, running,
175
+ // self-ticking game whose play state the mount had itself set to `playing`.
176
+ // A lying refusal is worse than a missing feature: it sends the reader to
177
+ // `vgai play`, which is not the thing that was wrong. The question this gate
178
+ // asks is "is a game surface live here", and a live ingest is one.
179
+ if (!isPlayModeActive() && !hasLiveDebugPlane() && !isIngestActive()) return notPlayingResult();
180
+ // Capture the ADDRESSED seat, not always the primary. `game.instance(id)`
181
+ // sends its id here, so `game.instance('2').screenshot()` grabs seat 2's game
182
+ // stack; an unaddressed call still captures the primary, and a caller that
183
+ // wants every seat captures each id in turn (one image per instance).
184
+ const instance = cmd['instance'] as string | undefined;
185
+ const container = getInstanceContainer(instance);
186
+ // WHICH SURFACE CARRIES THIS GAME'S PICTURE — a read, not a sniff, in both
187
+ // halves. `getInstanceCanvas` resolves the ELEMENT through
188
+ // `presentation-surface.ts` (the host's own mount stamp, then a self-booting
189
+ // game's contract, then the labelled measured sweep), and `medium` is what
190
+ // the adapter's region table DECLARES this project is presented on.
191
+ const canvas = getInstanceCanvas(instance);
192
+ const medium = presentationSurface(container).medium;
193
+ // A starved canvas is provably stale, but a React-only root has no canvas:
194
+ // its DOM is self-driven and `capturePlayComposite` has its own bounded
195
+ // on-demand paint fallback. The gate keys on the presentation surface
196
+ // EXISTING, which is the question it always meant to ask — and that existence
197
+ // check now reads a declaration before it measures anything.
198
+ const runtime = getPlayRuntimeAccess();
199
+ let loopRecoveryFrame = false;
200
+ let compositeFailure: unknown = null;
201
+ if (canvas && runtime?.loop.liveness === 'loop-starved') {
202
+ if (cmd['refreshStarvedFrame'] === true && runtime.runTicks) {
203
+ try {
204
+ runtime.runTicks(1, { render: 'last' });
205
+ // Stamp the RESULT, not just the log: a frame that exists because the
206
+ // capture drove the game is current but nobody was watching it, and
207
+ // every caller (CLI, poke surface, relay transport) has to be able to
208
+ // say so without inferring it from its own retry bookkeeping.
209
+ loopRecoveryFrame = true;
210
+ } catch (err) {
211
+ return structuredErrorResult(err);
212
+ }
213
+ } else {
214
+ return {
215
+ ok: false,
216
+ error:
217
+ "cannot capture a fresh game frame while the host loop reports 'loop-starved' — " +
218
+ 'inspect the separate visibility readings, let presentation resume, or request a ' +
219
+ 'deterministic loop-recovery frame',
220
+ data: { code: 'BRIDGE_SCREENSHOT_STALE' },
221
+ };
222
+ }
223
+ }
224
+ if (
225
+ container &&
226
+ typeof Image !== 'undefined' &&
227
+ typeof XMLSerializer !== 'undefined' &&
228
+ typeof container.getBoundingClientRect === 'function'
229
+ ) {
230
+ try {
231
+ const composite = await capturePlayComposite(container, {
232
+ canvasFrame: captureLiveCanvasFrame,
233
+ // The DECLARED medium, from the adapter's region table. `undefined`
234
+ // (no table loaded yet) leaves the compositor-settle heuristic on its
235
+ // own canvas count, exactly as before.
236
+ ...(medium === 'unknown' ? {} : { presentsOnCanvas: medium === 'canvas' }),
237
+ });
238
+ if (composite.layers.canvases === 0 && composite.layers.domOverlays === 0) {
239
+ throw new CaptureLayerError(
240
+ medium === 'dom' ? 'dom-overlay' : 'composite-output',
241
+ 'no game layers were mounted',
242
+ );
243
+ }
244
+ return screenshotResult(composite.base64, {
245
+ composite: true,
246
+ layers: composite.layers,
247
+ flatness: composite.flatness,
248
+ loopRecoveryFrame,
249
+ });
250
+ } catch (error) {
251
+ compositeFailure = error;
252
+ editorConsole.warn(
253
+ `Full-stack screenshot failed; returning the canvas-only fallback — ${error instanceof Error ? error.message : String(error)}`,
254
+ 'screenshot',
255
+ );
256
+ // Degrade to the canvas-only leg below — a HUD-less capture beats no
257
+ // capture, and `composite: false` keeps the degradation visible.
258
+ }
259
+ }
260
+ // A native React game has no canvas by design. The composite leg above is
261
+ // therefore the PRIMARY capture path for a React-only root, not an
262
+ // enhancement after a canvas precondition. Only fail once both the full
263
+ // game-stack capture and the canvas fallback are unavailable.
264
+ if (!canvas) {
265
+ const compositeReason =
266
+ compositeFailure instanceof Error ? compositeFailure.message : String(compositeFailure);
267
+ return {
268
+ ok: false,
269
+ error:
270
+ compositeFailure !== null
271
+ ? `full game-stack capture failed — ${compositeReason}; no canvas layer is available as a fallback`
272
+ : isIngestActive()
273
+ ? 'the ingest mount is live but no DOM overlay or canvas layer is mounted for capture'
274
+ : 'no play-mode DOM overlay or canvas layer is mounted yet',
275
+ data: { code: 'BRIDGE_SCREENSHOT_UNAVAILABLE' },
276
+ };
277
+ }
278
+ try {
279
+ // Same seam as the composite leg, for the same reason: a game's own canvas
280
+ // is unreadable this late, so ask for a same-frame copy first and fall back
281
+ // to reading the canvas itself.
282
+ const frameModule = await import('@volter/editor-core/live-canvas-frame');
283
+ const readable =
284
+ frameModule.readablePngSource(await frameModule.liveCanvasFrame(canvas)) ?? canvas;
285
+ const dataUrl = readable.toDataURL('image/png');
286
+ const comma = dataUrl.indexOf(',');
287
+ const base64 = comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
288
+ // The degraded leg gets the same honesty check as the composite one — a
289
+ // canvas-only frame of a buried camera is exactly as blank.
290
+ return screenshotResult(base64, {
291
+ composite: false,
292
+ flatness: sampleFlatness(readable, canvas.ownerDocument),
293
+ loopRecoveryFrame,
294
+ });
295
+ } catch (err) {
296
+ // e.g. a tainted canvas (a cross-origin texture loaded with no CORS
297
+ // headers, which WebGL refuses to read back even with
298
+ // preserveDrawingBuffer:true) — a distinct, rarer failure from "no
299
+ // canvas at all" above, surfaced honestly rather than swallowed.
300
+ return {
301
+ ok: false,
302
+ error:
303
+ `bridge-screenshot: canvas layer capture failed — ${
304
+ err instanceof Error ? err.message : String(err)
305
+ }` +
306
+ (compositeFailure === null
307
+ ? ''
308
+ : `; full game-stack capture also failed — ${
309
+ compositeFailure instanceof Error
310
+ ? compositeFailure.message
311
+ : String(compositeFailure)
312
+ }`),
313
+ data: { code: 'BRIDGE_SCREENSHOT_UNAVAILABLE' },
314
+ };
315
+ }
316
+ }