@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.
- package/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- package/src/xstate/xstate-layout.ts +76 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one slot that says an ingest mount is WAITING for the game's first
|
|
3
|
+
* frame, and whether that wait is parked because the tab is hidden.
|
|
4
|
+
*
|
|
5
|
+
* WHY IT EXISTS. The capture window is a budget of VISIBLE time
|
|
6
|
+
* (`@volter/threejs-runtime/adapter/ingest/visible-capture-window`), so a tab that boots in the
|
|
7
|
+
* background — the normal human path — no longer burns its window unable to
|
|
8
|
+
* draw. What it does instead is WAIT, potentially for as long as the human
|
|
9
|
+
* takes to come back to the tab, and a wait is exactly the state every door
|
|
10
|
+
* used to report as silence: nothing mounted, nothing failed, `vgai status`
|
|
11
|
+
* counting down a clock that was not running. So the wait is published:
|
|
12
|
+
* `collectState`'s `ingestCaptureWait` carries it, and `vgai status` prints
|
|
13
|
+
* "waiting for first visible frame — tab is hidden" instead of a countdown.
|
|
14
|
+
*
|
|
15
|
+
* A LIVE VIEW, never a snapshot — same rule as `IngestMount.realmLoopVerdict`
|
|
16
|
+
* and the `__vgaiIngest` hook's `reflected`/`sceneChildren`: the elapsed
|
|
17
|
+
* numbers move on their own, so a frozen copy starts lying the instant it is
|
|
18
|
+
* taken. The slot holds the window itself and derives the report at read time.
|
|
19
|
+
*
|
|
20
|
+
* RESOURCE OWNERSHIP: the MOUNT owns this slot. `mountIngestGame`'s
|
|
21
|
+
* `waitForCapture` sets it when the wait begins and clears it (with `null`) the
|
|
22
|
+
* moment the wait ends — captured, expired, or torn down — through the same
|
|
23
|
+
* `onWait` callback, so there is exactly one writer and no second teardown
|
|
24
|
+
* path. A second mount replaces the first's entry; there is only ever one wait.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { notifyLiveSessionsChanged } from '@volter/editor-core/live-session-registry';
|
|
28
|
+
import type { VisibleCaptureWindow } from '@volter/threejs-runtime/adapter/ingest/visible-capture-window';
|
|
29
|
+
|
|
30
|
+
/** The wait as `vgai status` reports it. */
|
|
31
|
+
export interface CaptureWaitStatus {
|
|
32
|
+
/** The world whose first frame is being waited for. */
|
|
33
|
+
readonly worldId: string;
|
|
34
|
+
/** The capture window, in VISIBLE milliseconds. */
|
|
35
|
+
readonly budgetMs: number;
|
|
36
|
+
/**
|
|
37
|
+
* Visible milliseconds spent so far — the only ones the window counts.
|
|
38
|
+
*
|
|
39
|
+
* There is deliberately no hidden-time counterpart here. The server holds
|
|
40
|
+
* the last snapshot a tab POSTed, and a hidden tab posts nothing while it
|
|
41
|
+
* waits, so a hidden-ms field would be frozen at whatever it was when the
|
|
42
|
+
* wait began and read as "0ms hidden" after an hour of it. This one cannot
|
|
43
|
+
* go stale in the state that matters: while the tab is hidden it is not
|
|
44
|
+
* moving. (Hidden time IS measured live in the page, where it is honest —
|
|
45
|
+
* the capture-timeout error quotes it.)
|
|
46
|
+
*/
|
|
47
|
+
readonly elapsedVisibleMs: number;
|
|
48
|
+
/** Whether the wait is parked RIGHT NOW because the tab is hidden. */
|
|
49
|
+
readonly hidden: boolean;
|
|
50
|
+
/** Whether the browser is currently presenting frames. Wider than hidden:
|
|
51
|
+
* WebKit also suspends an unfocused window while Page Visibility says visible. */
|
|
52
|
+
readonly suspended: boolean;
|
|
53
|
+
/** The observed browser condition parking the capture budget. */
|
|
54
|
+
readonly suspensionReason: 'hidden' | 'unfocused' | 'page-suspended' | null;
|
|
55
|
+
/** The wait in one sentence, for a reader who sees only this line. */
|
|
56
|
+
readonly reason: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ActiveWait {
|
|
60
|
+
readonly worldId: string;
|
|
61
|
+
readonly window: VisibleCaptureWindow;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let _wait: ActiveWait | null = null;
|
|
65
|
+
const _listeners = new Set<() => void>();
|
|
66
|
+
|
|
67
|
+
function notify(): void {
|
|
68
|
+
for (const listener of _listeners) listener();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe to wait start/end. Visibility transitions do NOT come through
|
|
73
|
+
* here — `command-listener.ts` already re-POSTs state on `visibilitychange`
|
|
74
|
+
* (its `reportPresence`), which is the same event this wait parks and resumes
|
|
75
|
+
* on, so a second listener for it would be a duplicate reporter of one event.
|
|
76
|
+
*/
|
|
77
|
+
export function subscribeToCaptureWait(callback: () => void): () => void {
|
|
78
|
+
_listeners.add(callback);
|
|
79
|
+
return () => {
|
|
80
|
+
_listeners.delete(callback);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Publish the running wait, or `null` when it ends. The mount's `onWait`. */
|
|
85
|
+
export function setCaptureWait(worldId: string, window: VisibleCaptureWindow | null): void {
|
|
86
|
+
_wait = window ? { worldId, window } : null;
|
|
87
|
+
notify();
|
|
88
|
+
// The state report's external-change channel (`command-listener.ts`
|
|
89
|
+
// re-reports on the registry's version).
|
|
90
|
+
notifyLiveSessionsChanged();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The live wait, derived at read time, or `null` when nothing is waiting. */
|
|
94
|
+
export function captureWaitStatus(): CaptureWaitStatus | null {
|
|
95
|
+
const active = _wait;
|
|
96
|
+
if (!active) return null;
|
|
97
|
+
const hidden = active.window.isHidden();
|
|
98
|
+
const suspended = active.window.isSuspended();
|
|
99
|
+
const suspensionReason = active.window.suspensionReason();
|
|
100
|
+
return {
|
|
101
|
+
worldId: active.worldId,
|
|
102
|
+
budgetMs: active.window.budgetMs,
|
|
103
|
+
elapsedVisibleMs: Math.round(active.window.elapsedVisibleMs()),
|
|
104
|
+
hidden,
|
|
105
|
+
suspended,
|
|
106
|
+
suspensionReason,
|
|
107
|
+
reason:
|
|
108
|
+
suspensionReason === 'hidden'
|
|
109
|
+
? 'waiting for first visible frame — tab is hidden (a hidden tab cannot render, so the ' +
|
|
110
|
+
'capture window is parked, not counting down; bring the tab to the foreground)'
|
|
111
|
+
: suspensionReason === 'unfocused'
|
|
112
|
+
? 'waiting for the browser to resume frame presentation — the editor window is ' +
|
|
113
|
+
'unfocused (WebKit can suspend requestAnimationFrame in this state, so the capture ' +
|
|
114
|
+
'window is parked; focus the editor window)'
|
|
115
|
+
: suspensionReason === 'page-suspended'
|
|
116
|
+
? 'waiting for the browser to restore the suspended editor page — the capture ' +
|
|
117
|
+
'window is parked, not counting down'
|
|
118
|
+
: suspended
|
|
119
|
+
? 'waiting for the browser to resume frame presentation — the capture window is parked'
|
|
120
|
+
: "waiting for the game's first rendered frame",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The play-time half of an ingest root that declares a design-time world
|
|
3
|
+
* (the root's `world` — see the manifest schema's own description).
|
|
4
|
+
*
|
|
5
|
+
* WHAT THIS EXISTS FOR. An ingest root used to mount at BOOT and stay mounted:
|
|
6
|
+
* the running game WAS the edit surface, so the Scene view showed the game's
|
|
7
|
+
* mount surfaces (a `Canvas` root with a nested `DOM UI` document) as its
|
|
8
|
+
* hierarchy roots, and whatever the game animated animated there. A scene is a
|
|
9
|
+
* single root by definition (owner, 2026-08-15) and Edit ≠ Play
|
|
10
|
+
* (ARCHITECTURE-CORE: LOADING CONSTRUCTS, PLAY RUNS), so for a root that names
|
|
11
|
+
* its own world component the two mounts split:
|
|
12
|
+
*
|
|
13
|
+
* EDIT — isolation documents (or `r3f-design-session.ts` for a named world
|
|
14
|
+
* component) mount each piece ALONE. Nothing ticks.
|
|
15
|
+
* PLAY — this module mounts the WHOLE GAME through the ordinary ingest
|
|
16
|
+
* routes (`mount-ingest-root.ts`), byte-for-byte the mount that used
|
|
17
|
+
* to happen at boot: its own entry, its own contract shim, its own
|
|
18
|
+
* composition, HUD, physics and loop. Stop tears it down and asks for
|
|
19
|
+
* one edit-mode rebuild, which recreates the design session.
|
|
20
|
+
*
|
|
21
|
+
* A root that declares neither a world component nor isolation-document
|
|
22
|
+
* scene tabs is untouched by all of this: it mounts at boot exactly as
|
|
23
|
+
* before, and {@link ingestMountIsDeferredToPlay} answers `false` for it.
|
|
24
|
+
*
|
|
25
|
+
* RESOURCE OWNERSHIP. The ingest SESSION is owned by `active-ingest.ts`, as it
|
|
26
|
+
* always was, and the one teardown path allowed to end it stays
|
|
27
|
+
* `exitActiveIngest()`. This module owns nothing but the decision of WHEN the
|
|
28
|
+
* existing mount runs; `_claimedPlay` below is a latch over that decision, so
|
|
29
|
+
* Stop only tears down a session THIS module started (an ordinary boot-time
|
|
30
|
+
* ingest keeps its own lifecycle).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { commandLine } from '@volter/editor-core/product-command';
|
|
34
|
+
import { queueEditModeRebuild } from '@volter/editor-core/authoring/edit-mode-authoring';
|
|
35
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
36
|
+
import { fetchGameManifest } from '@volter/editor-core/manifest-project';
|
|
37
|
+
import {
|
|
38
|
+
type ProjectAdapterFacet,
|
|
39
|
+
projectAdapterFacet,
|
|
40
|
+
subscribeProjectAdapter,
|
|
41
|
+
} from '@volter/editor-core/project-adapter';
|
|
42
|
+
import { sceneTabRow } from '@volter/editor-core/scene-document-plan';
|
|
43
|
+
import { ingestRoots } from '@volter/editor-project/adapter/manifest-interpreter';
|
|
44
|
+
import { activeIngest } from './active-ingest';
|
|
45
|
+
import {
|
|
46
|
+
beginDeferredIngestPlaySession,
|
|
47
|
+
deferredIngestPlayActive,
|
|
48
|
+
endDeferredIngestPlaySession,
|
|
49
|
+
} from './deferred-ingest-session';
|
|
50
|
+
import { getIngestPlayControl } from './ingest-play-control';
|
|
51
|
+
import { exitActiveIngest } from './unmount-ingest-root';
|
|
52
|
+
|
|
53
|
+
export { deferredIngestPlayActive };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Does Edit already own pieces for this ingest root, so the live game must
|
|
57
|
+
* wait for Play?
|
|
58
|
+
*
|
|
59
|
+
* Two declarations, same answer:
|
|
60
|
+
* - the root's `world` — a named design-time world component;
|
|
61
|
+
* - isolation-document rows on the adapter scene table — per-piece Edit
|
|
62
|
+
* tabs (bubbo Title / Game / Result).
|
|
63
|
+
*
|
|
64
|
+
* Either one means Edit mounts constructs, not the running game
|
|
65
|
+
* (ARCHITECTURE-CORE: LOADING CONSTRUCTS, PLAY RUNS). A self-booting
|
|
66
|
+
* ingest with neither still mounts at boot — that is the degenerate
|
|
67
|
+
* "smallest piece is the whole game" case.
|
|
68
|
+
*
|
|
69
|
+
*/
|
|
70
|
+
export async function ingestMountIsDeferredToPlay(): Promise<boolean> {
|
|
71
|
+
const manifest = await fetchGameManifest().catch(() => null);
|
|
72
|
+
if (!manifest) return false;
|
|
73
|
+
const declared = ingestRoots(manifest);
|
|
74
|
+
// Exactly one: a manifest declaring two ingest roots is already a named
|
|
75
|
+
// failure in `mount-ingest-root.ts`, and that error must keep being the one
|
|
76
|
+
// the user sees rather than being pre-empted by a silent deferral here.
|
|
77
|
+
if (declared.length !== 1) return false;
|
|
78
|
+
if (declared[0]!.world !== undefined) return true;
|
|
79
|
+
const facet = await adapterFacetWhenReady();
|
|
80
|
+
return isolationDocumentsDeclared(facet);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** How long boot waits for the adapter facet before deciding it has none. */
|
|
84
|
+
const FACET_WAIT_MS = 4000;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The scene table is published asynchronously (`startProjectAdapterLoad`).
|
|
88
|
+
* Boot must wait for that publish — a miss here is how isolation tabs exist
|
|
89
|
+
* AND the live game still mounts underneath them.
|
|
90
|
+
*/
|
|
91
|
+
function adapterFacetWhenReady(): Promise<ProjectAdapterFacet | null> {
|
|
92
|
+
const existing = projectAdapterFacet();
|
|
93
|
+
if (existing) return Promise.resolve(existing);
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
unsub();
|
|
97
|
+
resolve(projectAdapterFacet());
|
|
98
|
+
}, FACET_WAIT_MS);
|
|
99
|
+
const unsub = subscribeProjectAdapter(() => {
|
|
100
|
+
const facet = projectAdapterFacet();
|
|
101
|
+
if (!facet) return;
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
unsub();
|
|
104
|
+
resolve(facet);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isolationDocumentsDeclared(facet: ProjectAdapterFacet | null): boolean {
|
|
110
|
+
if (!facet) return false;
|
|
111
|
+
return sceneTabRow(facet.scenes).some((row) => row.plan.kind === 'isolation-document');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Does the live ingest session belong to a PLAY RUN this module started?
|
|
116
|
+
*
|
|
117
|
+
* Two surfaces ask, and both ask it about the same thing — whether ⏹ means
|
|
118
|
+
* anything here. An ingest that mounted at boot IS the editor's open document,
|
|
119
|
+
* so "stop" can only honestly mean "pause its loop" (`ingest-play-commands.ts`
|
|
120
|
+
* says exactly that, and the PlayBar's ingest toolbar offers no Stop). A
|
|
121
|
+
* deferred run inverts that premise: Play constructed it, so Stop ends it and
|
|
122
|
+
* hands the surface back to the design-time Scene.
|
|
123
|
+
*/
|
|
124
|
+
/**
|
|
125
|
+
* Play's mount for a deferred ingest root: run the ordinary ingest routes,
|
|
126
|
+
* then start the game through its own play control (the same ▶ the PlayBar
|
|
127
|
+
* drives for a boot-mounted ingest — contract-first `lifecycle.start`, else
|
|
128
|
+
* the wire event, plus the loop gate).
|
|
129
|
+
*
|
|
130
|
+
* Returns `false` when this project has no deferred ingest root, which is the
|
|
131
|
+
* signal for `play-mode.ts` to continue into its ordinary first-party boot.
|
|
132
|
+
*/
|
|
133
|
+
export async function mountDeferredIngestForPlay(store: EditorShellStore): Promise<boolean> {
|
|
134
|
+
if (!(await ingestMountIsDeferredToPlay())) return false;
|
|
135
|
+
// Edit isolation and the live game share the browser's Pixi module realm.
|
|
136
|
+
// Isolation may therefore have initialized the singleton Assets manager,
|
|
137
|
+
// while the untouched game quite correctly initializes it at boot. Reset at
|
|
138
|
+
// the explicit realm-ownership boundary: live Play now owns the manager and
|
|
139
|
+
// loads its own manifest without a duplicate-init warning. Existing Edit
|
|
140
|
+
// display objects retain their texture objects while hidden; Stop rebuilds
|
|
141
|
+
// the Edit presentation through the ordinary path below.
|
|
142
|
+
//
|
|
143
|
+
// ON THIS SURFACE'S OWN NAMESPACE (`../vite-plugin-module-doorways.ts`): the
|
|
144
|
+
// manager being reset is a MODULE-GLOBAL of one `pixi.js` instance, so a
|
|
145
|
+
// reset on the wrong instance resets nothing at all — the same shape as
|
|
146
|
+
// `owned-pixi-ticker-listeners.ts`'s `Ticker.shared`. Under the packaged
|
|
147
|
+
// runtime both halves of the sentence above live in the PROJECT's graph:
|
|
148
|
+
// `mount-isolated-pixi-screen.ts` initializes assets through the game's own
|
|
149
|
+
// module (`authoring/pixi-isolation-assets.ts` calls the game's
|
|
150
|
+
// `initialize`), and the game's boot does too. A static `pixi.js` here is
|
|
151
|
+
// the shell's third instance, which neither of them ever touched.
|
|
152
|
+
const manifest = await fetchGameManifest().catch(() => null);
|
|
153
|
+
const deferredCanvasRoot = manifest
|
|
154
|
+
? ingestRoots(manifest).some((root) => root.adapter.surface === 'canvas')
|
|
155
|
+
: false;
|
|
156
|
+
// …but ONLY when no full game boot owns the realm yet. When a held ingest
|
|
157
|
+
// session is live (a creation-site write materializes one — the write needs
|
|
158
|
+
// the live object), the shared singletons hold THE GAME'S OWN warm state,
|
|
159
|
+
// and this reset tears exactly half of it: the Assets cache empties while
|
|
160
|
+
// sibling registries the same boot populated (`@pixi/sound`'s alias table)
|
|
161
|
+
// stay full. The next boot then re-loads every bundle into duplicate-alias
|
|
162
|
+
// asserts, the load breaks partway, and the game never renders a frame —
|
|
163
|
+
// measured on bubbo-bubbo (2026-08-21): every ▶ after an authoring write
|
|
164
|
+
// died in the 30s capture timeout this way. A warm second boot instead
|
|
165
|
+
// finds its bundles cached (Assets.init warns duplicate-init and keeps the
|
|
166
|
+
// manifest, which is the same manifest) and renders. The reset stays for
|
|
167
|
+
// the case it was built for: isolation initialized the manager and no game
|
|
168
|
+
// boot ever ran, where the game's first boot deserves a virgin manager.
|
|
169
|
+
if (deferredCanvasRoot && !activeIngest()) {
|
|
170
|
+
const { resolveCanvasPixiForEditor } = await import('../host/canvas-entry-runtime');
|
|
171
|
+
const pixi = await resolveCanvasPixiForEditor();
|
|
172
|
+
pixi.Assets.reset();
|
|
173
|
+
}
|
|
174
|
+
const { launchIngestRoutes } = await import('./mount-ingest-root');
|
|
175
|
+
await launchIngestRoutes(store);
|
|
176
|
+
if (!activeIngest()) {
|
|
177
|
+
// The routes already reported the failure through the mount-failure
|
|
178
|
+
// report and reset the play surface; say why play produced nothing rather
|
|
179
|
+
// than leaving the caller to infer it from a quiet return.
|
|
180
|
+
throw new Error(
|
|
181
|
+
"Play could not start: this project's ingest root failed to mount — see the mount " +
|
|
182
|
+
`failure reported above (and in ${commandLine('status')}).`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
beginDeferredIngestPlaySession();
|
|
186
|
+
// The GAME document is where a running runtime presents (ARCHITECTURE-CORE:
|
|
187
|
+
// Game is a PLAY-TIME document). The three ingest mount leaves the editor on
|
|
188
|
+
// the Scene tab because, until now, that mount WAS edit mode; a play-time
|
|
189
|
+
// mount belongs on the play surface — which is also what opens the input
|
|
190
|
+
// gate (`mount-three-ingest-root.ts`'s `ingestInputActive` requires the play
|
|
191
|
+
// tab), so the game is drivable rather than merely visible.
|
|
192
|
+
store.setActiveViewportTab('play');
|
|
193
|
+
getIngestPlayControl()?.play();
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Stop's half. Returns `true` when it tore a play-claimed ingest down, so
|
|
199
|
+
* `exitPlayMode` can skip a first-party teardown that has nothing to tear.
|
|
200
|
+
* Idempotent; a no-op for a boot-mounted ingest.
|
|
201
|
+
*/
|
|
202
|
+
export function exitDeferredIngestPlay(store: EditorShellStore): boolean {
|
|
203
|
+
if (!deferredIngestPlayActive()) return false;
|
|
204
|
+
endDeferredIngestPlaySession();
|
|
205
|
+
// `exitActiveIngest` is the ONE teardown path for the session (this module's
|
|
206
|
+
// ownership note) — it unmounts by kind, clears the authoring override and
|
|
207
|
+
// the adopted scene, and resets the play surface.
|
|
208
|
+
exitActiveIngest();
|
|
209
|
+
store.setPlayState('stopped');
|
|
210
|
+
store.setActiveViewportTab('edit');
|
|
211
|
+
// Edit mode is rebuilt through the ONE authoritative reinstall path
|
|
212
|
+
// (`the world root's stage`'s `installAll`), which is what recreates the R3F design
|
|
213
|
+
// session over the world component.
|
|
214
|
+
queueEditModeRebuild();
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SESSION FACT for a deferred ingest Play run.
|
|
3
|
+
*
|
|
4
|
+
* Kept in a dependency-free leaf because both the deferred mount orchestrator
|
|
5
|
+
* and the editor-wide mode predicate must read it. Putting the latch in either
|
|
6
|
+
* owner creates a cycle through `ingest-play-control` or `play-mode` and, more
|
|
7
|
+
* importantly, lets one consumer silently forget this is a Play SESSION even
|
|
8
|
+
* though it is not a first-party `play-mode.ts` session.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let active = false;
|
|
12
|
+
|
|
13
|
+
export function deferredIngestPlayActive(): boolean {
|
|
14
|
+
return active;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function beginDeferredIngestPlaySession(): void {
|
|
18
|
+
active = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function endDeferredIngestPlaySession(): void {
|
|
22
|
+
active = false;
|
|
23
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored ingest discovery by id.
|
|
3
|
+
*
|
|
4
|
+
* The three sibling of `surface-canvas.ts` (the pixi lane, which is the idiom
|
|
5
|
+
* benchmark this file is modelled on): a game vendored under
|
|
6
|
+
* `public/ingest/<id>/` carries its own declarative `vgai.project.json`, and
|
|
7
|
+
* its id resolves it into the SAME `IngestGame` descriptor every other three
|
|
8
|
+
* ingest route produces. Adding a fourth vendored game is a folder plus a
|
|
9
|
+
* manifest — no edit to this file, no per-game page, no branch anywhere.
|
|
10
|
+
*
|
|
11
|
+
* ## Why `fetch`, where the in-tree lane uses `import.meta.glob`
|
|
12
|
+
*
|
|
13
|
+
* `registry.ts` globs IN-TREE fixture manifests under
|
|
14
|
+
* `packages/editor/src/ingest/games/`, which Vite knows statically (
|
|
15
|
+
* ONE manifest-driven registry, with only the per-surface MOUNT still split —
|
|
16
|
+
* `surface-three.ts`/`surface-canvas.ts`/`surface-dom.ts`).
|
|
17
|
+
* These manifests live in `public/`, which is deliberately OUTSIDE the module
|
|
18
|
+
* graph in both builds: Vite serves it verbatim in dev and copies it into
|
|
19
|
+
* `dist/` at build, and importing from it is explicitly unsupported (that
|
|
20
|
+
* verbatim serving is exactly why the vendored bundles can keep their absolute
|
|
21
|
+
* `import '/ingest/<id>/three-rNNN.module.js'` specifiers un-rewritten). A
|
|
22
|
+
* root-relative `fetch` is therefore the id-generic route that works
|
|
23
|
+
* identically on the dev server and on the deployed hosted editor — no
|
|
24
|
+
* dev-server middleware, nothing to build.
|
|
25
|
+
*
|
|
26
|
+
* There is deliberately no enumeration here: the caller supplies the id,
|
|
27
|
+
* and a 404 means "not a vendored three ingest", cleanly. A gallery that needs
|
|
28
|
+
* the LIST wants a build-time aggregate (the
|
|
29
|
+
* `project-scripts-by-example.generated.ts` pattern), not a runtime directory scan.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
isSafeIngestId,
|
|
34
|
+
PUBLIC_INGEST_BASE,
|
|
35
|
+
publicIngestManifestUrl,
|
|
36
|
+
} from '../host/staged-projects';
|
|
37
|
+
import { extractUpstreamPin } from '@volter/game-runtime/adapter/ingest/upstream-pin';
|
|
38
|
+
import type { IngestGame2D } from '@volter/game-runtime/pixi/ingest';
|
|
39
|
+
import { declaredRoots } from '@volter/editor-project/adapter/manifest-interpreter';
|
|
40
|
+
import { loadGameManifest } from '@volter/editor-project/manifest/load';
|
|
41
|
+
import { composeIngestLoad, ingestDataWriter } from './entry-load';
|
|
42
|
+
import { servedEntryLoader, servedModuleLoader } from './served-bundle';
|
|
43
|
+
import type { IngestGame } from './types';
|
|
44
|
+
|
|
45
|
+
/** What a vendored three ingest manifest resolves to. */
|
|
46
|
+
export interface PublicIngestResolution {
|
|
47
|
+
game: IngestGame;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What a vendored canvas ingest manifest resolves to. */
|
|
51
|
+
export interface PublicCanvasIngestResolution {
|
|
52
|
+
game: IngestGame2D;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Import a module served out of a vendored game's folder, in the host realm.
|
|
56
|
+
* The loader itself is `served-bundle.ts`'s — shared with the LOCAL session
|
|
57
|
+
* route so both doors onto one folder run it identically. */
|
|
58
|
+
function publicIngestModuleLoader(folderId: string, relPath: string): () => Promise<unknown> {
|
|
59
|
+
return servedModuleLoader(`${PUBLIC_INGEST_BASE}${folderId}/`, relPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The game's own entry module, preceded by its `contractShim` when it declares one. */
|
|
63
|
+
function publicIngestLoad(
|
|
64
|
+
folderId: string,
|
|
65
|
+
entry: string,
|
|
66
|
+
contractShim: string | undefined,
|
|
67
|
+
): () => Promise<unknown> {
|
|
68
|
+
return composeIngestLoad(
|
|
69
|
+
servedEntryLoader(`${PUBLIC_INGEST_BASE}${folderId}/`, entry),
|
|
70
|
+
contractShim === undefined ? undefined : publicIngestModuleLoader(folderId, contractShim),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The single ingest root a vendored game's manifest must declare. */
|
|
75
|
+
function readVendoredIngestRoot(
|
|
76
|
+
folderId: string,
|
|
77
|
+
raw: unknown,
|
|
78
|
+
expect: { surface: 'three' | 'canvas'; identity: 'ingest-three' | 'ingest-pixi' },
|
|
79
|
+
) {
|
|
80
|
+
const manifest = loadGameManifest(raw, { configurationKinds: 'defer' });
|
|
81
|
+
const roots = declaredRoots(manifest);
|
|
82
|
+
if (roots.length !== 1) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`vendored ingest discovery: "${folderId}/vgai.project.json" must declare exactly one root ` +
|
|
85
|
+
`(found ${roots.length}).`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const root = roots[0]!;
|
|
89
|
+
if (
|
|
90
|
+
root.surface !== expect.surface ||
|
|
91
|
+
root.adapter.type !== 'ingest' ||
|
|
92
|
+
root.adapter.identity !== expect.identity
|
|
93
|
+
) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`vendored ingest discovery: "${folderId}/vgai.project.json" must declare one ` +
|
|
96
|
+
`${expect.surface} { ingest } root (found surface "${root.surface}", adapter identity ` +
|
|
97
|
+
`"${root.adapter.identity}").`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (!root.entry) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`vendored ingest discovery: "${folderId}" has no \`entry\` — an ingest root mounts the ` +
|
|
103
|
+
"game's own entry module.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return { manifest, root, adapter: root.adapter, entry: root.entry };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the descriptor from a folder id and its parsed manifest JSON. Pure —
|
|
111
|
+
* the unit-testable half, with every rejection a NAMED error rather than a
|
|
112
|
+
* silent skip (the anti-shim rule: an unmountable manifest must say why).
|
|
113
|
+
*/
|
|
114
|
+
export function buildPublicIngestGame(folderId: string, raw: unknown): PublicIngestResolution {
|
|
115
|
+
const { manifest, root, adapter, entry } = readVendoredIngestRoot(folderId, raw, {
|
|
116
|
+
surface: 'three',
|
|
117
|
+
identity: 'ingest-three',
|
|
118
|
+
});
|
|
119
|
+
const game: IngestGame = {
|
|
120
|
+
id: root.id,
|
|
121
|
+
name: manifest.name,
|
|
122
|
+
description: root.description ?? '',
|
|
123
|
+
gameVersion: manifest.version,
|
|
124
|
+
load: publicIngestLoad(folderId, entry, adapter.contractShim),
|
|
125
|
+
...ingestDataWriter(adapter.dataWriter, (rel) => publicIngestModuleLoader(folderId, rel)),
|
|
126
|
+
...(adapter.assets !== undefined ? { assets: adapter.assets } : {}),
|
|
127
|
+
...(adapter.captureTimeoutMs !== undefined
|
|
128
|
+
? { captureTimeoutMs: adapter.captureTimeoutMs }
|
|
129
|
+
: {}),
|
|
130
|
+
...(adapter.domStubs !== undefined ? { domStubs: adapter.domStubs } : {}),
|
|
131
|
+
};
|
|
132
|
+
return { game };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Canvas-surface sibling of {@link buildPublicIngestGame}. `ingest-pixi` is
|
|
137
|
+
* the existing schema identity for canvas ingest roots; nothing here infers
|
|
138
|
+
* Pixi from it. The mount recognizes the actual library in the live realm
|
|
139
|
+
* (`mount-canvas-ingest-root.ts` -> `authoring/canvas-runtime-recognition.ts`)
|
|
140
|
+
* and fails loudly when no structural adapter exists.
|
|
141
|
+
*/
|
|
142
|
+
export function buildPublicCanvasIngestGame(
|
|
143
|
+
folderId: string,
|
|
144
|
+
raw: unknown,
|
|
145
|
+
): PublicCanvasIngestResolution {
|
|
146
|
+
const { manifest, root, adapter, entry } = readVendoredIngestRoot(folderId, raw, {
|
|
147
|
+
surface: 'canvas',
|
|
148
|
+
identity: 'ingest-pixi',
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
game: {
|
|
152
|
+
id: root.id,
|
|
153
|
+
name: manifest.name,
|
|
154
|
+
description: root.description ?? '',
|
|
155
|
+
load: publicIngestLoad(folderId, entry, adapter.contractShim),
|
|
156
|
+
...(adapter.captureTimeoutMs !== undefined
|
|
157
|
+
? { captureTimeoutMs: adapter.captureTimeoutMs }
|
|
158
|
+
: {}),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve a vendored game id against `public/ingest/<id>/vgai.project.json`.
|
|
165
|
+
*
|
|
166
|
+
* `null` means "no such vendored three ingest" (no manifest served at that
|
|
167
|
+
* path) — the caller falls through to its own unknown-id report. A manifest
|
|
168
|
+
* that EXISTS but is unmountable THROWS, naming the reason: silently treating a
|
|
169
|
+
* malformed vendored manifest as absent is exactly the failure the anti-shim
|
|
170
|
+
* rule forbids.
|
|
171
|
+
*/
|
|
172
|
+
export async function fetchPublicIngestGame(
|
|
173
|
+
id: string,
|
|
174
|
+
fetchImpl: typeof fetch = globalThis.fetch,
|
|
175
|
+
): Promise<PublicIngestResolution | null> {
|
|
176
|
+
if (!isSafeIngestId(id)) return null;
|
|
177
|
+
const url = publicIngestManifestUrl(id);
|
|
178
|
+
let res: Response;
|
|
179
|
+
try {
|
|
180
|
+
res = await fetchImpl(url);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
if (!res.ok) return null;
|
|
185
|
+
// The dev server answers an unknown path with the editor's index.html rather
|
|
186
|
+
// than a 404, so an OK response is not by itself proof of a manifest — parse
|
|
187
|
+
// failure on this route means "not a vendored ingest", not a broken game.
|
|
188
|
+
let raw: unknown;
|
|
189
|
+
try {
|
|
190
|
+
raw = await res.json();
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return buildPublicIngestGame(id, raw);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Resolve a vendored canvas game, preserving the same 404 rules. */
|
|
198
|
+
export async function fetchPublicCanvasIngestGame(
|
|
199
|
+
id: string,
|
|
200
|
+
fetchImpl: typeof fetch = globalThis.fetch,
|
|
201
|
+
): Promise<PublicCanvasIngestResolution | null> {
|
|
202
|
+
if (!isSafeIngestId(id)) return null;
|
|
203
|
+
let response: Response;
|
|
204
|
+
try {
|
|
205
|
+
response = await fetchImpl(publicIngestManifestUrl(id));
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
if (!response.ok) return null;
|
|
210
|
+
let raw: unknown;
|
|
211
|
+
try {
|
|
212
|
+
raw = await response.json();
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return buildPublicCanvasIngestGame(id, raw);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Read THIS vendored game's own `UPSTREAM.md` pin (R5's overlay-identity check
|
|
221
|
+
* — an overlay is bound to the upstream commit the game was vendored at, not
|
|
222
|
+
* only to a static manifest `version`).
|
|
223
|
+
*
|
|
224
|
+
* Deliberately NOT `ingest/mount-ingest-root.ts`'s `readVendoredUpstreamPin`, which fetches
|
|
225
|
+
* the OPEN PROJECT's root `/UPSTREAM.md`: on this route the open project is
|
|
226
|
+
* whatever the editor happens to have open, and its pin has nothing to do with
|
|
227
|
+
* the vendored game being mounted. Every failure resolves to `null` — a missing
|
|
228
|
+
* or unparseable record is reported as "no pin", never fabricated.
|
|
229
|
+
*/
|
|
230
|
+
export async function fetchPublicIngestUpstreamPin(
|
|
231
|
+
id: string,
|
|
232
|
+
fetchImpl: typeof fetch = globalThis.fetch,
|
|
233
|
+
): Promise<string | null> {
|
|
234
|
+
if (!isSafeIngestId(id)) return null;
|
|
235
|
+
try {
|
|
236
|
+
const res = await fetchImpl(`${PUBLIC_INGEST_BASE}${id}/UPSTREAM.md`);
|
|
237
|
+
if (!res.ok) return null;
|
|
238
|
+
return extractUpstreamPin(await res.text());
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dataset key a host-made DOM stub carries (`ingest-root-adapter.ts`
|
|
3
|
+
* creates them for a game's declared `domStubs`), so a served bundle's staged
|
|
4
|
+
* page can retire the stub whose id it supplies itself (`served-html-boot.ts`).
|
|
5
|
+
* Shared here because the two sides must agree on one mark.
|
|
6
|
+
*/
|
|
7
|
+
export const DOM_STUB_MARK = 'vgaiDomStub';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an ingested game's modules are run: the game's own entry module, in the
|
|
3
|
+
* editor's realm, preceded by the host-added contract module the manifest names
|
|
4
|
+
* in `ingest.contractShim` when it declares one.
|
|
5
|
+
*
|
|
6
|
+
* ORDER IS THE WHOLE CONTRACT. The shim assigns `window.vgaiGame` synchronously
|
|
7
|
+
* at top level (see the field's schema description), so it must be evaluated
|
|
8
|
+
* BEFORE the game's entry — a game that reads the declaration during its own
|
|
9
|
+
* module evaluation would otherwise see nothing, and a shim that lazily
|
|
10
|
+
* `import()`s a game module inside a verb closure would reorder the game's own
|
|
11
|
+
* module side effects if it were evaluated after. Every discovery frontend
|
|
12
|
+
* (the in-tree fixture glob, the manifest/project-folder resolver, the vendored
|
|
13
|
+
* by-id fetcher) composes its two loaders through this one function rather than
|
|
14
|
+
* open-coding the sequence three times.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The manifest's `ingest.dataWriter`, resolved against however THIS discovery
|
|
18
|
+
* frontend serves the game's folder — the same three-way split `load` already
|
|
19
|
+
* has (globbed fixture, `public/` url, `/@fs/` project folder), spelled once so
|
|
20
|
+
* a fourth frontend cannot forget the field.
|
|
21
|
+
*
|
|
22
|
+
* The writer is deliberately NOT composed into `load`: it is not part of
|
|
23
|
+
* booting the game and must not be evaluated at mount. It is imported lazily,
|
|
24
|
+
* the first time an edit needs it, and a game that never has one edited never
|
|
25
|
+
* pays for it.
|
|
26
|
+
*/
|
|
27
|
+
export function ingestDataWriter(
|
|
28
|
+
specifier: string | undefined,
|
|
29
|
+
loader: (relPath: string) => () => Promise<unknown>,
|
|
30
|
+
): { dataWriter?: { specifier: string; load: () => Promise<unknown> } } {
|
|
31
|
+
return specifier === undefined ? {} : { dataWriter: { specifier, load: loader(specifier) } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The game's OWN source modules this root declares — see
|
|
36
|
+
* {@link import('./types').IngestGame.sourceModules}. Spelled once here, beside
|
|
37
|
+
* `ingestDataWriter`, for the same reason: every discovery frontend must fill
|
|
38
|
+
* the field the same way or the lane decision's scope silently differs by route.
|
|
39
|
+
*/
|
|
40
|
+
export function declaredSourceModules(
|
|
41
|
+
entry: string | undefined,
|
|
42
|
+
worldEntry: string | undefined,
|
|
43
|
+
): string[] {
|
|
44
|
+
return [entry, worldEntry].filter((path): path is string => typeof path === 'string');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function composeIngestLoad(
|
|
48
|
+
entry: () => Promise<unknown>,
|
|
49
|
+
contractShim: (() => Promise<unknown>) | undefined,
|
|
50
|
+
): () => Promise<unknown> {
|
|
51
|
+
if (!contractShim) return entry;
|
|
52
|
+
return async () => {
|
|
53
|
+
await contractShim();
|
|
54
|
+
return entry();
|
|
55
|
+
};
|
|
56
|
+
}
|