@volter/editor-game 0.5.66 → 0.5.68

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 (239) hide show
  1. package/contributions/audio-unlock.service.ts +2 -2
  2. package/contributions/autoplay.service.ts +1 -1
  3. package/contributions/bridge.command.ts +3 -3
  4. package/contributions/build-player.document.tsx +95 -0
  5. package/contributions/canvas/component-board.service.ts +16 -0
  6. package/contributions/canvas/design-time-mount.service.ts +45 -0
  7. package/contributions/canvas-story-capture.service.ts +12 -0
  8. package/contributions/edit-mode-audio.service.ts +2 -2
  9. package/contributions/edit-mode-networking.service.ts +1 -1
  10. package/contributions/gameplay.command.ts +4 -4
  11. package/contributions/generation.service.ts +1 -1
  12. package/contributions/godot.style.ts +26 -6
  13. package/contributions/godot.view.ts +6 -0
  14. package/contributions/ingest.service.ts +2 -2
  15. package/contributions/instances.command.ts +1 -1
  16. package/contributions/navmesh.menu.ts +1 -1
  17. package/contributions/network-observer.service.ts +14 -0
  18. package/contributions/play.command.ts +10 -4
  19. package/contributions/react/component-board.service.ts +1 -1
  20. package/contributions/react/design-time-mount.service.ts +1 -1
  21. package/contributions/scene-document.service.ts +3 -3
  22. package/contributions/state-watch.menu.ts +1 -1
  23. package/contributions/state-watch.utility.tsx +1 -1
  24. package/contributions/team-playtest.service.ts +4 -4
  25. package/contributions/three/component-board.service.ts +1 -1
  26. package/contributions/three/component-verbs.command.ts +6 -6
  27. package/contributions/three/three-authoring.service.ts +8 -5
  28. package/contributions/three-story-capture.service.ts +12 -0
  29. package/contributions/unity.style.ts +17 -7
  30. package/contributions/unity.view.ts +6 -0
  31. package/contributions/unreal.style.ts +10 -2
  32. package/contributions/unreal.view.ts +5 -0
  33. package/package.json +21 -10
  34. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  35. package/src/asset-budget/asset-budget-model.ts +1 -1
  36. package/src/audio/AudioDebuggerPanel.tsx +1 -1
  37. package/src/bridge/dispatch.ts +14 -14
  38. package/src/bridge/live-frames.ts +1 -1
  39. package/src/bridge/screenshot.ts +3 -3
  40. package/src/build/BuildProfilesPanel.tsx +14 -3
  41. package/src/build/build-session.ts +35 -0
  42. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +749 -0
  43. package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
  44. package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
  45. package/src/canvas/canvas-design-mount.ts +524 -0
  46. package/src/canvas/design-time-canvas-mount.ts +79 -0
  47. package/src/coverage/live-authoring-surface.ts +5 -5
  48. package/src/coverage/live-project-verbs.ts +1 -1
  49. package/src/coverage/native-system-coverage.ts +5 -5
  50. package/src/coverage/root-coverage.ts +1 -1
  51. package/src/coverage/session-coverage.ts +3 -3
  52. package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
  53. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
  54. package/src/edit-mode/edit-mode-audio.ts +4 -4
  55. package/src/edit-mode/edit-mode-networking.ts +2 -2
  56. package/src/game-document/GameCaptureFrameButton.tsx +1 -1
  57. package/src/game-document/GameDocument.tsx +2 -2
  58. package/src/game-document/GamePanel.tsx +4 -4
  59. package/src/game-document/InstanceInspectorPicker.tsx +2 -2
  60. package/src/game-document/crowd-debug.ts +1 -1
  61. package/src/game-document/device-preview.ts +10 -11
  62. package/src/game-document/physics-debug.ts +1 -1
  63. package/src/generation/GenerationActivity.tsx +3 -3
  64. package/src/generation/generation-documents.tsx +5 -5
  65. package/src/generation/generation-jobs.ts +1 -1
  66. package/src/host/adapter-runtime-bindings.ts +96 -5
  67. package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
  68. package/src/host/authoring/gesture-persist.ts +1 -1
  69. package/src/host/authoring/ingest-data-writer.ts +1 -1
  70. package/src/host/authoring/ingest-source-persistence.ts +4 -4
  71. package/src/host/authoring/mounted-authoring.ts +4 -4
  72. package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
  73. package/src/host/authoring/pixi-authoring-adapter.ts +178 -31
  74. package/src/host/authoring/pixi-creatable-kinds.ts +62 -0
  75. package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
  76. package/src/host/authoring/pixi-live-write-target.ts +4 -4
  77. package/src/host/authoring/pixi-source-identity.ts +3 -3
  78. package/src/host/authoring/pixi-source-write-target.ts +1709 -0
  79. package/src/host/authoring/pixi-still-presentation.ts +1 -1
  80. package/src/host/authoring/pixi-structure-history.ts +2 -2
  81. package/src/host/authoring/pixi-transform-channels.ts +16 -14
  82. package/src/host/authoring/source-persistence-backend.ts +3 -3
  83. package/src/host/authoring/struct-write-pipe.ts +1 -1
  84. package/src/host/binding-resolver.ts +8 -9
  85. package/src/host/browser-transpile.ts +1 -1
  86. package/src/host/canvas-entry-runtime.ts +58 -47
  87. package/src/host/canvas-preview-frames.ts +482 -0
  88. package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
  89. package/src/host/components/HeaderTelemetry.tsx +4 -4
  90. package/src/host/components/PixiIsolationSceneContent.tsx +11 -11
  91. package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
  92. package/src/host/components/frame-debugger-model.ts +2 -2
  93. package/src/host/components/header-telemetry-model.ts +2 -2
  94. package/src/host/components/scene-document.tsx +14 -14
  95. package/src/host/components/utility-view-state.ts +1 -1
  96. package/src/host/components/world-root-stage-binding.tsx +12 -12
  97. package/src/host/components/world-root-stage.ts +70 -49
  98. package/src/host/coverage/system-adapter-coverage.ts +3 -4
  99. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
  100. package/src/host/document-preview-three.ts +1 -1
  101. package/src/host/entry-adjudication.ts +6 -6
  102. package/src/host/game-css-scope-transform.ts +4 -0
  103. package/src/host/game-realm-page.ts +1 -1
  104. package/src/host/gameplay-export.ts +25 -14
  105. package/src/host/gameplay-recording.ts +5 -5
  106. package/src/host/gated-globals.ts +2 -2
  107. package/src/host/history/json-history-resource.ts +1 -1
  108. package/src/host/projection/pixi.ts +24 -2
  109. package/src/host/r3f-entry-runtime.ts +65 -34
  110. package/src/host/react-mount-runtime.ts +7 -48
  111. package/src/host/realm-services.ts +1 -1
  112. package/src/host/roots/canvas-root.tsx +373 -0
  113. package/src/host/roots/r3f-root.tsx +473 -0
  114. package/src/host/roots/react-root.ts +9 -43
  115. package/src/host/served-bundle-runtime-modules.ts +3 -19
  116. package/src/host/server-log-bridge.ts +2 -2
  117. package/src/host/stories/mounted-story-viewport-source.ts +1 -1
  118. package/src/host/stories/pixi-story-model.ts +30 -0
  119. package/src/host/stories/story-media-captures.ts +46 -0
  120. package/src/host/stories/story-media-presence.ts +3 -3
  121. package/src/host/stories/story-pixi-preview.ts +408 -0
  122. package/src/host/stories/story-three-preview.ts +806 -0
  123. package/src/host/stories/three-story-captures.ts +35 -0
  124. package/src/host/story-three-preview-runtime.ts +56 -0
  125. package/src/host/use-active-performance-source.ts +2 -2
  126. package/src/host/viewport-pose-memory.ts +1 -1
  127. package/src/host/viewport-root-presentation.ts +6 -5
  128. package/src/ingest/active-ingest.ts +1 -1
  129. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  130. package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
  131. package/src/ingest/deferred-ingest-play.ts +6 -6
  132. package/src/ingest/discovery-public-ingest.ts +2 -2
  133. package/src/ingest/ingest-boot-viewport.ts +2 -2
  134. package/src/ingest/ingest-canvas-scene-document.tsx +9 -9
  135. package/src/ingest/ingest-canvas-scene.ts +3 -3
  136. package/src/ingest/ingest-evidence-hook.ts +1 -1
  137. package/src/ingest/ingest-frame-snapshot.ts +1 -1
  138. package/src/ingest/ingest-play-control.ts +1 -1
  139. package/src/ingest/ingest-render-debug.ts +10 -10
  140. package/src/ingest/ingest-siblings.ts +13 -25
  141. package/src/ingest/module-mode.ts +14 -14
  142. package/src/ingest/mount-canvas-ingest-root.ts +21 -21
  143. package/src/ingest/mount-coverage.ts +2 -2
  144. package/src/ingest/mount-dom-ingest-root.ts +11 -11
  145. package/src/ingest/mount-ingest-root.ts +8 -8
  146. package/src/ingest/mount-three-ingest-root.ts +8 -8
  147. package/src/ingest/resolve-canvas.ts +1 -1
  148. package/src/ingest/served-html-boot.ts +1 -1
  149. package/src/ingest/surface-canvas.ts +1 -1
  150. package/src/ingest/unmount-ingest-root.ts +5 -5
  151. package/src/navmesh/navmesh-handler.ts +24 -16
  152. package/src/network/NetworkInspectorPanel.tsx +939 -37
  153. package/src/network/network-inspector-model.ts +20 -2
  154. package/src/play/play-log-events.ts +1 -1
  155. package/src/play/play-mode.ts +76 -133
  156. package/src/play/play-recording.ts +1 -1
  157. package/src/play/react-play-live-authoring.ts +3 -3
  158. package/src/play/run-selection.ts +93 -0
  159. package/src/play-bar/PlayBar.tsx +20 -39
  160. package/src/profiler/FrameDebuggerPanel.tsx +1 -1
  161. package/src/profiler/PerformancePanel.tsx +3 -3
  162. package/src/react/design-time-react-mount.ts +23 -65
  163. package/src/react/dom-authoring-adapter.ts +9 -9
  164. package/src/react/react-inspector-section.tsx +5 -5
  165. package/src/react/react-world-authoring-adapter.ts +11 -11
  166. package/src/react/story-documents/story-documents.tsx +9 -9
  167. package/src/react/ui-board-document.tsx +7 -7
  168. package/src/react/ui-component-board.ts +2 -2
  169. package/src/runtime/adapter/audio-meter.ts +21 -0
  170. package/src/runtime/adapter/first-party-audio-system.ts +230 -0
  171. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
  172. package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
  173. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
  174. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
  175. package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
  176. package/src/runtime/adapter/native-debug-module.ts +498 -0
  177. package/src/runtime/audio/bus-mixer.ts +161 -0
  178. package/src/runtime/audio/pose-guard.ts +80 -0
  179. package/src/runtime/core/frame-pacing.ts +126 -0
  180. package/src/runtime/core/game-loop.ts +225 -0
  181. package/src/runtime/core/game-scoped-slot.ts +28 -0
  182. package/src/runtime/core/seeded-random.ts +162 -0
  183. package/src/runtime/core/sim-clock.ts +391 -0
  184. package/src/runtime/core/system-runner.ts +269 -0
  185. package/src/runtime/core/types.ts +104 -0
  186. package/src/runtime/create-runtime.ts +1128 -0
  187. package/src/runtime/debug-bridge.ts +570 -0
  188. package/src/runtime/debug-registry.ts +899 -0
  189. package/src/runtime/dev/chrome-trace.ts +153 -0
  190. package/src/runtime/dev/instruments.ts +403 -0
  191. package/src/runtime/dev/logger.ts +119 -0
  192. package/src/runtime/dev/performance-profiler.ts +367 -0
  193. package/src/runtime/dev/register-render-vitals.ts +276 -0
  194. package/src/runtime/dev/render-census.ts +354 -0
  195. package/src/runtime/dev/render-debug-adapter.ts +218 -0
  196. package/src/runtime/dev/render-memory.ts +226 -0
  197. package/src/runtime/dev/render-vitals.ts +338 -0
  198. package/src/runtime/dev/static-batch-advisor.ts +188 -0
  199. package/src/runtime/dev/webgl-frame-capture.ts +366 -0
  200. package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
  201. package/src/runtime/dev-build.ts +47 -0
  202. package/src/runtime/game.ts +1636 -0
  203. package/src/runtime/gameplay-rng-trap.ts +135 -0
  204. package/src/runtime/host-context.ts +64 -0
  205. package/src/runtime/input-router.ts +182 -0
  206. package/src/runtime/mount-manifest.ts +480 -0
  207. package/src/runtime/pixi/authoring.ts +706 -0
  208. package/src/runtime/pixi/ingest.ts +116 -0
  209. package/src/runtime/pixi/physics-registry.ts +49 -0
  210. package/src/runtime/pixi/render-pass-bracket.ts +117 -0
  211. package/src/runtime/pixi/scene-capture.ts +179 -0
  212. package/src/runtime/pixi/system-adapters.ts +69 -0
  213. package/src/runtime/playtest.ts +22 -0
  214. package/src/runtime/presentation.ts +141 -0
  215. package/src/runtime/render-control.ts +642 -0
  216. package/src/runtime/render-seed.ts +77 -0
  217. package/src/runtime/run-ticks-settled.ts +73 -0
  218. package/src/runtime/setup/setup-audio.ts +72 -0
  219. package/src/services/audio-pose-guard.ts +2 -2
  220. package/src/services/game-audio.ts +152 -0
  221. package/src/services/game-network.ts +767 -0
  222. package/src/services/game-physics.ts +334 -0
  223. package/src/state-watch/StateWatchPanel.tsx +1 -1
  224. package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
  225. package/src/three/authoring/constraint-inspector-section.tsx +6 -5
  226. package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
  227. package/src/three/authoring/oid-source-persistence.ts +7 -7
  228. package/src/three/authoring/r3f-design-session.ts +58 -54
  229. package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
  230. package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
  231. package/src/three/authoring/three-authoring-adapter.ts +29 -29
  232. package/src/three/component-verbs/extract-menu.ts +7 -6
  233. package/src/three/component-verbs/fork-menu.ts +7 -6
  234. package/src/three/component-verbs/internals-menu.ts +2 -2
  235. package/src/three/story-documents/three-story-documents.tsx +13 -13
  236. package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
  237. package/src/three/three-board/board-scene.ts +6 -6
  238. package/src/three/three-board/three-component-board.ts +2 -2
  239. package/src/services/game-audio-unlock.ts +0 -48
@@ -0,0 +1,197 @@
1
+ /**
2
+ * ONE `DebugAdapter` out of several projections of the same mount.
3
+ *
4
+ * Two feeders exist today and they are peers, not layers: the game's own
5
+ * declared system surface (`contract-debug-adapter.ts`, from
6
+ * `window.vgaiGame.systems`) and its adapter module's observation table
7
+ * (`observation-debug-adapter.ts`). Both project onto the ONE runtime door the
8
+ * editor and `vgai eval` already read (`SystemAdapters.debug`), so the merge
9
+ * belongs here rather than in a second slot — a consumer must not be able to
10
+ * tell a name's provenance, which is the whole point of both projections.
11
+ *
12
+ * ## A collision REFUSES the name; it never shadows it
13
+ *
14
+ * When two feeders declare the same provider or command name, one of them would
15
+ * otherwise win silently and an agent would read the wrong thing with every log
16
+ * line still saying success. So the merged adapter:
17
+ *
18
+ * - LISTS the name once (it is declared, and hiding it would be its own lie);
19
+ * - REFUSES to answer it with a coded `DEBUG_NAME_COLLISION` naming BOTH
20
+ * sources, at `state()` / `invoke()` and per-key inside `stateAll()`;
21
+ * - reports the collisions to the caller ({@link MergedDebugAdapters}) so the
22
+ * mount can say so out loud where a person will see it, instead of the
23
+ * refusal waiting to be discovered by whoever calls the name first.
24
+ *
25
+ * The refusal is per-NAME, not per-mount, deliberately: killing the mount would
26
+ * cost a user their whole game to protect against one ambiguous read, and the
27
+ * per-name refusal already gives the same guarantee — nothing is silently
28
+ * shadowed — with none of the collateral.
29
+ */
30
+
31
+ import type {
32
+ DebugAdapter,
33
+ DebugCommandInfo,
34
+ TickStampedEvent,
35
+ } from '@volter/editor-project/adapter/system-adapter';
36
+ import { DebugError } from '../../debug-registry';
37
+
38
+ /** One feeder, with the words a collision message names it by. */
39
+ export interface DebugAdapterSource {
40
+ /** How this projection is named to a reader — its provenance, in prose. */
41
+ readonly label: string;
42
+ /** `null` when this feeder projected nothing (the honest floor both return). */
43
+ readonly adapter: DebugAdapter | null;
44
+ }
45
+
46
+ export interface MergedDebugAdapters {
47
+ /** The one door, or `null` when no feeder projected anything. */
48
+ readonly adapter: DebugAdapter | null;
49
+ /** One sentence per colliding name, naming both sources. Empty is the norm. */
50
+ readonly collisions: readonly string[];
51
+ }
52
+
53
+ function collisionError(name: string, labels: readonly string[]): DebugError {
54
+ return new DebugError(
55
+ 'DEBUG_NAME_COLLISION',
56
+ `"${name}" is declared by more than one source (${labels.join(' and ')}) — refusing to ` +
57
+ 'answer rather than picking one silently. Rename it in whichever source you own.',
58
+ { name, sources: [...labels] },
59
+ );
60
+ }
61
+
62
+ /** Names each feeder claims, in listing order, de-duplicated within a feeder. */
63
+ function claimedNames(
64
+ sources: readonly DebugAdapterSource[],
65
+ face: 'state' | 'command',
66
+ ): {
67
+ order: string[];
68
+ labelsByName: Map<string, string[]>;
69
+ } {
70
+ const order: string[] = [];
71
+ const labelsByName = new Map<string, string[]>();
72
+ for (const source of sources) {
73
+ if (!source.adapter) continue;
74
+ const names =
75
+ face === 'state'
76
+ ? source.adapter.providers().map((provider) => provider.name)
77
+ : source.adapter.commands().map((command) => command.name);
78
+ for (const name of new Set(names)) {
79
+ if (!labelsByName.has(name)) {
80
+ labelsByName.set(name, []);
81
+ order.push(name);
82
+ }
83
+ labelsByName.get(name)?.push(source.label);
84
+ }
85
+ }
86
+ return { order, labelsByName };
87
+ }
88
+
89
+ /**
90
+ * Fold every projection of one mount onto a single {@link DebugAdapter}.
91
+ *
92
+ * `events()` concatenates, because an event ring is append-only and two rings
93
+ * genuinely both happened — there is no name to collide on. Everything else is
94
+ * name-keyed and therefore goes through the collision rule above.
95
+ */
96
+ export function mergeDebugAdapters(sources: readonly DebugAdapterSource[]): MergedDebugAdapters {
97
+ const live = sources.filter((source) => source.adapter !== null);
98
+ if (live.length === 0) return { adapter: null, collisions: [] };
99
+ if (live.length === 1) return { adapter: live[0]!.adapter, collisions: [] };
100
+
101
+ const providers = claimedNames(live, 'state');
102
+ const commands = claimedNames(live, 'command');
103
+
104
+ const collisions: string[] = [];
105
+ for (const [face, claimed] of [
106
+ ['state provider', providers],
107
+ ['command', commands],
108
+ ] as const) {
109
+ for (const name of claimed.order) {
110
+ const labels = claimed.labelsByName.get(name) ?? [];
111
+ if (labels.length > 1) {
112
+ collisions.push(`${face} "${name}" is declared by ${labels.join(' and ')}`);
113
+ }
114
+ }
115
+ }
116
+
117
+ /** The feeder that owns `name`, or a thrown collision error when several do. */
118
+ const ownerOf = (
119
+ claimed: ReturnType<typeof claimedNames>,
120
+ name: string,
121
+ face: 'state' | 'command',
122
+ ): DebugAdapter | null => {
123
+ const labels = claimed.labelsByName.get(name);
124
+ if (!labels) return null;
125
+ if (labels.length > 1) throw collisionError(name, labels);
126
+ return (
127
+ live.find((source) => {
128
+ const owned =
129
+ face === 'state'
130
+ ? source.adapter?.providers().some((provider) => provider.name === name)
131
+ : source.adapter?.commands().some((command) => command.name === name);
132
+ return owned === true;
133
+ })?.adapter ?? null
134
+ );
135
+ };
136
+
137
+ return {
138
+ adapter: {
139
+ providers: () =>
140
+ live.flatMap((source) => source.adapter?.providers() ?? []).filter(uniqueByName()),
141
+
142
+ state: (name: string): unknown => {
143
+ const owner = ownerOf(providers, name, 'state');
144
+ if (!owner) {
145
+ throw new DebugError(
146
+ 'STATE_PROVIDER_NOT_FOUND',
147
+ `No state provider named "${name}" is declared by this game.`,
148
+ { registered: providers.order },
149
+ );
150
+ }
151
+ return owner.state(name);
152
+ },
153
+
154
+ stateAll: (): Record<string, unknown> => {
155
+ const out: Record<string, unknown> = {};
156
+ for (const name of providers.order) {
157
+ try {
158
+ out[name] = ownerOf(providers, name, 'state')?.state(name);
159
+ } catch (err) {
160
+ out[name] = { __error: String(err) };
161
+ }
162
+ }
163
+ return out;
164
+ },
165
+
166
+ commands: (): DebugCommandInfo[] =>
167
+ live.flatMap((source) => source.adapter?.commands() ?? []).filter(uniqueByName()),
168
+
169
+ invoke: async (name: string, args: unknown[]): Promise<unknown> => {
170
+ const owner = ownerOf(commands, name, 'command');
171
+ if (!owner) {
172
+ throw new DebugError(
173
+ 'DEBUG_COMMAND_NOT_REGISTERED',
174
+ `No command named "${name}" is declared by this game.`,
175
+ { registered: commands.order },
176
+ );
177
+ }
178
+ return owner.invoke(name, args);
179
+ },
180
+
181
+ events: (sinceSeq?: number): TickStampedEvent[] =>
182
+ live.flatMap((source) => source.adapter?.events(sinceSeq) ?? []),
183
+ },
184
+ collisions,
185
+ };
186
+ }
187
+
188
+ /** Keep the FIRST listing of each name — the merged listing advertises a name
189
+ * once, whatever `state()`/`invoke()` then decide to do with it. */
190
+ function uniqueByName<T extends { name: string }>(): (value: T) => boolean {
191
+ const seen = new Set<string>();
192
+ return (value) => {
193
+ if (seen.has(value.name)) return false;
194
+ seen.add(value.name);
195
+ return true;
196
+ };
197
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Projects an ADAPTER's declared observation slots
3
+ * (`AdapterDefinition.observation`, `adapter/adapter-module.ts`) onto the host's
4
+ * existing {@link DebugAdapter} — the one seam `game.providers()` /
5
+ * `game.state()` / `game.commands()` already read through
6
+ * (`command-listener.ts`'s `dispatchBridgeMethod` → `getActiveSystems().debug`).
7
+ *
8
+ * This is the SIBLING of `contract-debug-adapter.ts` and deliberately the same
9
+ * shape: a projection rather than a second door. The two differ only in WHO
10
+ * declared the surface — the game itself, on `window.vgaiGame.systems`, or the
11
+ * game's adapter module beside it — and a consumer downstream must not be able
12
+ * to tell them apart. `merge-debug-adapters.ts` is what folds both onto the one
13
+ * slot.
14
+ *
15
+ * ## What `game` is
16
+ *
17
+ * The host's to hand over, and for an INGEST mount it is the REALM the game's
18
+ * own modules ran in. That is not a convenience: an ingest mount runs the game
19
+ * in the editor's own realm (`game-contract.ts`'s `readGameContract` says so —
20
+ * "the realm every ingest mount runs the game's modules in, where the game and
21
+ * the host share one global"), a vendored bundle's adapter is HOST-realm by the
22
+ * placement rule, and the only handles such a game publishes are the ones on
23
+ * that realm — its runtime's own public registry, or a host-build seam beside
24
+ * it. Handing the realm is therefore handing the game everything it actually
25
+ * exposes; handing anything narrower would make the adapter reach for `window`
26
+ * itself, which is the same read with the provenance hidden.
27
+ *
28
+ * ## The two-state rule, kept
29
+ *
30
+ * Declaring a slot and answering it are separate facts. A declaration with NO
31
+ * `answer` is still listed and refuses BY NAME when read — never omitted,
32
+ * because omitting it would report "the game has no such state" when the truth
33
+ * is "the table declared it and nothing binds it yet". Same for a throwing
34
+ * `answer`: it propagates (exactly as a first-party provider's does), and only
35
+ * `stateAll()` isolates it per key.
36
+ *
37
+ * ## What this deliberately does NOT do
38
+ *
39
+ * - **Validate arguments.** A declared command's `answer` receives the invoked
40
+ * argument list after the game handle; there is no Zod tuple beside a foreign
41
+ * game, so a verb validates itself and throws its own error, surfaced wrapped
42
+ * as `DEBUG_COMMAND_FAILED`. Identical to the contract projection's stance.
43
+ * - **Fabricate an event ring.** Nothing emits behind a declaration, so
44
+ * `events()` is empty — accurate, not a stub.
45
+ */
46
+
47
+ import type { ObservationDeclaration } from '@volter/editor-project/adapter/adapter-module';
48
+ import type {
49
+ DebugAdapter,
50
+ DebugCommandInfo,
51
+ TickStampedEvent,
52
+ } from '@volter/editor-project/adapter/system-adapter';
53
+ import { DebugError } from '../../debug-registry';
54
+
55
+ /**
56
+ * An adapter-declared read is DERIVED by the host-side table from the game's own
57
+ * handles — which is precisely what `assisted` means in the tier vocabulary
58
+ * (`game-contract.ts`: "`observable` is a value the game already computes,
59
+ * `assisted` is one the shim derives"). Reporting `observable` would claim the
60
+ * game itself publishes the value.
61
+ */
62
+ const DECLARED_TIER = 'assisted' as const;
63
+
64
+ function unanswered(id: string, kind: string): DebugError {
65
+ return new DebugError(
66
+ 'OBSERVATION_UNANSWERED',
67
+ `Observation "${id}" (${kind}) is declared by this game's adapter with no \`answer\` — ` +
68
+ 'the slot exists and nothing binds it yet, which is not the same as the game having none.',
69
+ { id, kind },
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Build a {@link DebugAdapter} over an adapter's observation declarations, or
75
+ * `null` when it declares none. `null` is the honest floor — the same one
76
+ * `createContractDebugAdapter` returns — so a game that declared nothing keeps
77
+ * reporting `DEBUG_ADAPTER_UNAVAILABLE` rather than serving an empty adapter
78
+ * that reads as "this game has no state".
79
+ */
80
+ export function createObservationDebugAdapter(
81
+ declarations: readonly ObservationDeclaration[] | undefined | null,
82
+ game: unknown,
83
+ ): DebugAdapter | null {
84
+ const all = declarations ?? [];
85
+ if (all.length === 0) return null;
86
+
87
+ // Snapshot into id-keyed maps once, exactly as the contract projection does:
88
+ // a later duplicate shadows an earlier one and the listing de-duplicates with
89
+ // it, so a listed id can never resolve differently than it reads.
90
+ const byProvider = new Map(all.filter((d) => d.kind === 'state').map((d) => [d.id, d]));
91
+ const byCommand = new Map(all.filter((d) => d.kind === 'command').map((d) => [d.id, d]));
92
+
93
+ return {
94
+ providers: () => [...byProvider.keys()].map((name) => ({ name, tier: DECLARED_TIER })),
95
+
96
+ state: (name: string): unknown => {
97
+ const declaration = byProvider.get(name);
98
+ if (!declaration) {
99
+ throw new DebugError(
100
+ 'STATE_PROVIDER_NOT_FOUND',
101
+ `No state provider named "${name}" is declared by this game's adapter.`,
102
+ { registered: [...byProvider.keys()] },
103
+ );
104
+ }
105
+ if (!declaration.answer) throw unanswered(name, 'state');
106
+ return declaration.answer(game);
107
+ },
108
+
109
+ stateAll: (): Record<string, unknown> => {
110
+ const out: Record<string, unknown> = {};
111
+ for (const [name, declaration] of byProvider) {
112
+ // One throwing declaration must not cost the caller every other
113
+ // reading — the same per-key isolation the first-party registry gives.
114
+ try {
115
+ out[name] = declaration.answer
116
+ ? declaration.answer(game)
117
+ : (() => {
118
+ throw unanswered(name, 'state');
119
+ })();
120
+ } catch (err) {
121
+ out[name] = { __error: String(err) };
122
+ }
123
+ }
124
+ return out;
125
+ },
126
+
127
+ commands: (): DebugCommandInfo[] =>
128
+ [...byCommand.keys()].map((name) => ({
129
+ name,
130
+ // An ingested game runs entirely in the browser realm the host mounted
131
+ // it in; there is no server leg for its verbs to route to.
132
+ locus: byCommand.get(name)?.locus ?? ('client' as const),
133
+ })),
134
+
135
+ invoke: async (name: string, args: unknown[]): Promise<unknown> => {
136
+ const declaration = byCommand.get(name);
137
+ if (!declaration) {
138
+ throw new DebugError(
139
+ 'DEBUG_COMMAND_NOT_REGISTERED',
140
+ `No command named "${name}" is declared by this game's adapter.`,
141
+ { registered: [...byCommand.keys()] },
142
+ );
143
+ }
144
+ if (!declaration.answer) throw unanswered(name, 'command');
145
+ try {
146
+ // The game handle first, then the invoked argument list — a declared
147
+ // verb is a function OF the mounted game, which is the whole reason
148
+ // `answer` takes one.
149
+ return await (declaration.answer as (game: unknown, ...rest: unknown[]) => unknown)(
150
+ game,
151
+ ...args,
152
+ );
153
+ } catch (err) {
154
+ throw new DebugError('DEBUG_COMMAND_FAILED', `Command "${name}" threw: ${String(err)}`, {
155
+ name,
156
+ });
157
+ }
158
+ },
159
+
160
+ events: (): TickStampedEvent[] => [],
161
+ };
162
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Upstream-pin extraction — R5 ((c)/ §7.1 item 9): the fix for the overlay identity
3
+ * gap the structural-id + `authoredAgainst.gameVersion` scheme (T7.7/D9) does not
4
+ * close. A re-vendor of a vendored upstream game that keeps a slot's shape (index +
5
+ * three.js type + name unchanged) reapplies a saved overlay onto a semantically
6
+ * different object with `applied: 1, orphanedIds: []` — silent wrong-meaning success
7
+ * — because `authoredAgainst.gameVersion` is the MANIFEST's own `version` (hand-set
8
+ * by whoever wrote the ingest manifest, e.g. every `public/ingest/*` vendored game
9
+ * ships `"version": "0.1.0"` regardless of which upstream commit it vendors), never
10
+ * tied to the thing that actually changes on a re-vendor: the `UPSTREAM.md` pin.
11
+ *
12
+ * This module is the pure, dependency-free half (parse text -> pin, or
13
+ * `null`): NO file I/O here, so it is usable from any context (editor
14
+ * save/apply-time hooks over a fetched `/UPSTREAM.md`, a future Node-side
15
+ * CLI check, tests). READ-ONLY by construction — it has no writer; this
16
+ * codebase never generates or edits an `UPSTREAM.md` (the anti-shim rule:
17
+ * adapters never fabricate first-party data, and a game's own vendoring
18
+ * provenance is exactly that).
19
+ *
20
+ * Format (established convention, not invented here — see every
21
+ * `public/ingest/*\/UPSTREAM.md`'s `## Upstream` section, e.g.
22
+ * `public/ingest/tanks/UPSTREAM.md`):
23
+ *
24
+ * ## Upstream
25
+ *
26
+ * - Repo: https://github.com/colyseus/realtime-tanks-demo
27
+ * - Commit: **`6339493130b4b1d4d28f0f52d17b2fba738c7d47`**
28
+ *
29
+ * i.e. a Markdown bullet whose label is `Commit:` and whose value is a bold,
30
+ * backtick-code full (or abbreviated) git SHA. Every BUNDLE-vendored game in
31
+ * this repo (`tanks`, `simcity`) follows this exact shape. Source-tree
32
+ * vendored games carry their pin in `vendor/games/<id>.UPSTREAM.lock`'s
33
+ * machine-readable `commit` instead and never reach this parser.
34
+ */
35
+
36
+ /** Matches `- Commit: **\`<sha>\`**` (any amount of internal whitespace, case-insensitive hex). */
37
+ const COMMIT_BULLET_RE = /-\s*Commit:\s*\*\*`([0-9a-fA-F]{7,40})`\*\*/;
38
+
39
+ /**
40
+ * Extract the pinned upstream commit hash from a vendored game's
41
+ * `UPSTREAM.md` prose, or `null` if the file doesn't follow the convention
42
+ * (or wasn't found — the caller decides what a missing file means; this
43
+ * function only parses text it's handed). Never throws — an unparseable
44
+ * document is an honest `null`, not an error (anti-shim: no guessing, no
45
+ * fabricating a pin from a game folder that doesn't declare one, e.g. an
46
+ * externally-authored non-vendored project opened via the CLI-on-folder
47
+ * route).
48
+ */
49
+ export function extractUpstreamPin(markdown: string): string | null {
50
+ return COMMIT_BULLET_RE.exec(markdown)?.[1] ?? null;
51
+ }