@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,897 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IngestRootAdapter — the {@link RootAdapter} for an UNMODIFIED external three.js
|
|
3
|
+
* game. Its `mount()` is the host-and-ingest path: install the render accessor
|
|
4
|
+
* trap on the shared `three`, run the unmodified game (its own renderer/scene/
|
|
5
|
+
* camera/rAF), capture its live scene on the first frame, and return a
|
|
6
|
+
* {@link MountedThreeRoot} whose `authoring` is the three authoring adapter
|
|
7
|
+
* over that live `Object3D` tree, parameterized by what the captured graph
|
|
8
|
+
* MEASURES as: a world carrying serve-time source stamps gets OID identity plus
|
|
9
|
+
* JSX write-back ({@link oidSourceThree}), one without them keeps
|
|
10
|
+
* structural-path identity and creation-site persistence
|
|
11
|
+
* ({@link structuralThree}). See {@link countStampedObjects} for why that
|
|
12
|
+
* is a measurement rather than a manifest field. `drivesOwnLoop` is true — the
|
|
13
|
+
* host does not tick it.
|
|
14
|
+
*
|
|
15
|
+
* This is the runtime half of the Phase-B inversion: an unmodified game becomes a
|
|
16
|
+
* peer implementer of the SAME `RootAdapter`/`MountedThreeRoot` contract first-party
|
|
17
|
+
* content uses — no separate editor data model.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { authoringOidOf } from '@volter/editor-core/authoring/component-instance-root';
|
|
21
|
+
import { setIngestDataWriter } from '../../host/authoring/ingest-data-writer';
|
|
22
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
23
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
24
|
+
import { GAME_SURFACE_CONTAINMENT_CSS } from '../../host/game-realm-page';
|
|
25
|
+
import { clearGameSurface, gameLoopGate, setGameSurface } from '../../host/gated-globals';
|
|
26
|
+
import { authoringJournal } from '../../host/history/json-history-resource';
|
|
27
|
+
import { clearPresentationSurface, recordPresentationSurface } from '@volter/editor-core/presentation-surface';
|
|
28
|
+
import { getCurrentProject } from '@volter/editor-core/project-manager';
|
|
29
|
+
import { clearRootReadiness, recordRootReadiness } from '@volter/editor-core/readiness';
|
|
30
|
+
import {
|
|
31
|
+
type SameRealmLoopGate,
|
|
32
|
+
type SameRealmLoopVerdict,
|
|
33
|
+
verifySameRealmLoopControl,
|
|
34
|
+
} from '../../host/same-realm-loop-gate';
|
|
35
|
+
import { ensureScopedGameStyles } from '@volter/editor-core/scoped-game-css';
|
|
36
|
+
import { resolveThreeIngestRuntimeForEditor } from '../../host/three-ingest-runtime';
|
|
37
|
+
import type { OidEntry } from '@volter/editor-core/ui-source/oid-transform';
|
|
38
|
+
import { createHttpSourceWriteBackend } from '@volter/editor-core/ui-source/source-write-backend';
|
|
39
|
+
import { serverRecordsSourceWrites } from '@volter/editor-core/ui-source/tier-source-write-backend';
|
|
40
|
+
import { clearWorldAdoption, worldAdoptionRecorder } from '@volter/editor-core/world-adoption';
|
|
41
|
+
import { markGameCssScope } from '@volter/editor-sdk/session/game-css-scope';
|
|
42
|
+
import type { MountedThreeRoot } from '@volter/editor-project/adapter';
|
|
43
|
+
import {
|
|
44
|
+
readGamePresentation,
|
|
45
|
+
readGameReady,
|
|
46
|
+
readGameWorld,
|
|
47
|
+
} from '@volter/editor-project/adapter/ingest/game-contract';
|
|
48
|
+
import { describeMountFailure } from '@volter/editor-project/adapter/ingest/mount-readiness';
|
|
49
|
+
import { formatLoopGateMessage } from '@volter/editor-project/adapter/loop-gate-report';
|
|
50
|
+
import {
|
|
51
|
+
oidSourceThree,
|
|
52
|
+
structuralThree,
|
|
53
|
+
type ThreeAuthoringAdapter,
|
|
54
|
+
} from '../../three/authoring/three-authoring-adapter';
|
|
55
|
+
import { isHostRenderer } from '@volter/editor-threejs/viewport/renderer-ownership';
|
|
56
|
+
import {
|
|
57
|
+
type CapturedThreeRenderer,
|
|
58
|
+
installSceneCapture,
|
|
59
|
+
type SceneCaptureHandle,
|
|
60
|
+
} from '@volter/threejs-runtime/adapter/ingest/scene-capture';
|
|
61
|
+
// TYPES ONLY. Every VALUE this module needs from `three` — the namespace the
|
|
62
|
+
// capture trap is installed on, its `DefaultLoadingManager`, and the two addon
|
|
63
|
+
// renderer classes — comes from `resolveThreeIngestRuntimeForEditor()` instead,
|
|
64
|
+
// because under the packaged runtime the shell's copy is not the one the
|
|
65
|
+
// ingested game's own modules resolve. See `../three-ingest-runtime.ts`.
|
|
66
|
+
import type * as THREE from 'three';
|
|
67
|
+
import { setCaptureWait } from '../capture-wait-report';
|
|
68
|
+
import { DOM_STUB_MARK } from '../dom-stub-mark';
|
|
69
|
+
import { ingestGameRealmWindow, readIngestGameContract } from '../game-contract-realm';
|
|
70
|
+
import { installGamePointerLockGate, releaseGamePointerLock } from '../game-pointer-lock';
|
|
71
|
+
import { claimHostSurfaceBox, hostSurfaceBackingSize } from '../host-surface-box';
|
|
72
|
+
import { clearIngestFrameSource } from '../ingest-frame-snapshot';
|
|
73
|
+
import { wireIngestSystems } from '../ingest-render-debug';
|
|
74
|
+
import type { IngestGame } from '../types';
|
|
75
|
+
|
|
76
|
+
export interface MountIngestOptions {
|
|
77
|
+
/**
|
|
78
|
+
* How long to wait for the game's first captured frame before the mount FAILS
|
|
79
|
+
* by name. A game that needs a long boot raises it.
|
|
80
|
+
*/
|
|
81
|
+
captureTimeoutMs?: number | undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A mounted ingest game plus the host-side handles the editor needs to tear down. */
|
|
85
|
+
export interface IngestMount {
|
|
86
|
+
game: MountedThreeRoot;
|
|
87
|
+
/** The live-scene authoring adapter. */
|
|
88
|
+
authoring: ThreeAuthoringAdapter;
|
|
89
|
+
hostEl: HTMLElement;
|
|
90
|
+
/** Live capture stats (draw count, child count) for proof/telemetry. */
|
|
91
|
+
capture: SceneCaptureHandle;
|
|
92
|
+
/** The captured scene (== game.scene). */
|
|
93
|
+
scene: THREE.Scene;
|
|
94
|
+
/**
|
|
95
|
+
* The measured loop verdict, read LIVE (a function, not a snapshot — the
|
|
96
|
+
* probe re-runs at every pause, so a frozen value would start lying the
|
|
97
|
+
* moment the game's shape changed); `null` before the first pause, because a
|
|
98
|
+
* verdict is a measurement and there has not been one yet.
|
|
99
|
+
*/
|
|
100
|
+
realmLoopVerdict?: (() => SameRealmLoopVerdict | null) | undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* D10/T7.6 loop-gate honesty check, extracted for direct unit testing
|
|
105
|
+
* builds the `setPaused` a mounted
|
|
106
|
+
* ingest game exposes to `Game.play.pause()`/`resume()` — separate from
|
|
107
|
+
* the authoring adapter's own edit-time `loop.pause()`/`loop.resume()`
|
|
108
|
+
* (gizmo freeze/unfreeze), which always calls through regardless of whether
|
|
109
|
+
* the loop is genuinely gateable.
|
|
110
|
+
*
|
|
111
|
+
* S-5 (the SimCity ingest ledger) rewrote both halves of this.
|
|
112
|
+
*
|
|
113
|
+
* COVERAGE. Pausing used to be `loop.pause()` alone — `setAnimationLoop(null)`,
|
|
114
|
+
* which reaches exactly one loop driver. SimCity renders through
|
|
115
|
+
* `setAnimationLoop` and SIMULATES through `setInterval(this.simulate, 1000)`,
|
|
116
|
+
* so pause froze the picture while the city kept building (measured: the game
|
|
117
|
+
* still advanced after the gate reported success). A pause now also HOLDS the
|
|
118
|
+
* same-realm scheduling gate (`../ingest/same-realm-loop-gate.ts`), which the
|
|
119
|
+
* dev-server prelude has shadowed into every game module.
|
|
120
|
+
*
|
|
121
|
+
* HONESTY. The old check asked `capture.getAnimationLoop(renderer) !== null` —
|
|
122
|
+
* whether the game had ever DECLARED a loop through the renderer — and warned
|
|
123
|
+
* only when it had not. That is a declaration, not a measurement, and it is why
|
|
124
|
+
* a gate that controlled none of SimCity's simulation still "reported success".
|
|
125
|
+
* Every pause now runs the probe (`verifySameRealmLoopControl`): control is
|
|
126
|
+
* claimed only when something was actually withheld, nothing gated ran, and the
|
|
127
|
+
* game renderer's own frame counter did not advance. Anything else warns loudly
|
|
128
|
+
* with the measured reason.
|
|
129
|
+
*/
|
|
130
|
+
export function createIngestLoopGate(
|
|
131
|
+
capture: Pick<SceneCaptureHandle, 'getAnimationLoop' | 'getDrawCount'>,
|
|
132
|
+
renderer: CapturedThreeRenderer,
|
|
133
|
+
worldId: string,
|
|
134
|
+
loop: { pause(): void; resume(): void },
|
|
135
|
+
opts: {
|
|
136
|
+
/** The same-realm scheduling gate (`gated-globals.ts`) — `null` when none
|
|
137
|
+
* is installed, which the probe reports as `self-driven`. Injected so this
|
|
138
|
+
* is unit-testable with a fake gate and no browser. */
|
|
139
|
+
realmGate?: SameRealmLoopGate | null;
|
|
140
|
+
/** Called with the MEASURED verdict after each pause. */
|
|
141
|
+
onVerdict?: (verdict: SameRealmLoopVerdict) => void;
|
|
142
|
+
verifyWindowMs?: number;
|
|
143
|
+
sleep?: (ms: number) => Promise<void>;
|
|
144
|
+
} = {},
|
|
145
|
+
): { setPaused(paused: boolean): void; lastVerdict(): SameRealmLoopVerdict | null } {
|
|
146
|
+
let lastVerdict: SameRealmLoopVerdict | null = null;
|
|
147
|
+
return {
|
|
148
|
+
setPaused(paused: boolean) {
|
|
149
|
+
if (!paused) {
|
|
150
|
+
loop.resume();
|
|
151
|
+
// Release the scheduling gate LAST so a parked simulation tick never
|
|
152
|
+
// runs against a renderer that has not been handed its loop back.
|
|
153
|
+
opts.realmGate?.release();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// S-5: pausing is BOTH halves. `setAnimationLoop(null)` stops the game's
|
|
157
|
+
// rendering; the same-realm gate parks the scheduling its simulation runs
|
|
158
|
+
// on. Freezing only the first is what let a "gated" SimCity keep building
|
|
159
|
+
// its city — its sim tick is a bare `setInterval`, which
|
|
160
|
+
// `setAnimationLoop(null)` does not touch and the old declaration-only
|
|
161
|
+
// check (`getAnimationLoop(renderer) !== null`) never even looked at.
|
|
162
|
+
loop.pause();
|
|
163
|
+
opts.realmGate?.hold();
|
|
164
|
+
// MEASURE, never assume — the honesty gate. A verdict is reported for
|
|
165
|
+
// every pause, including the `self-driven` ones the old check could not
|
|
166
|
+
// produce (it only ever warned about a game that never called
|
|
167
|
+
// `setAnimationLoop`, and said nothing at all otherwise).
|
|
168
|
+
void verifySameRealmLoopControl({
|
|
169
|
+
gate: opts.realmGate ?? null,
|
|
170
|
+
progress: () => renderer.info?.render.frame ?? capture.getDrawCount(),
|
|
171
|
+
...(opts.verifyWindowMs !== undefined ? { windowMs: opts.verifyWindowMs } : {}),
|
|
172
|
+
...(opts.sleep !== undefined ? { sleep: opts.sleep } : {}),
|
|
173
|
+
}).then((verdict) => {
|
|
174
|
+
lastVerdict = verdict;
|
|
175
|
+
opts.onVerdict?.(verdict);
|
|
176
|
+
if (verdict.loop === 'gated') return;
|
|
177
|
+
// A NEW native console.warn site — suppressed to keep this task's
|
|
178
|
+
// diff at zero NEW lint warnings (same reasoning as
|
|
179
|
+
// `runtime/game.ts`'s `reportGateShortfallOnce`).
|
|
180
|
+
// biome-ignore lint/suspicious/noConsole: see comment above
|
|
181
|
+
console.warn(
|
|
182
|
+
formatLoopGateMessage({
|
|
183
|
+
worldId,
|
|
184
|
+
reason: capture.getAnimationLoop(renderer)
|
|
185
|
+
? verdict.reason
|
|
186
|
+
: `${verdict.reason} (this game also never called renderer.setAnimationLoop)`,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
lastVerdict: () => lastVerdict,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* How many objects in the captured world carry a serve-time source stamp —
|
|
197
|
+
* THE measurement that decides which authoring lane this mount gets.
|
|
198
|
+
*
|
|
199
|
+
* Exported for the unit test that proves the decision is made on the graph
|
|
200
|
+
* rather than on a game id: a stamped scene picks the OID lane, an unstamped
|
|
201
|
+
* one keeps the structural lane.
|
|
202
|
+
*/
|
|
203
|
+
export function countStampedObjects(scene: THREE.Object3D): number {
|
|
204
|
+
let stamped = 0;
|
|
205
|
+
scene.traverse((object) => {
|
|
206
|
+
if (authoringOidOf(object) !== undefined) stamped++;
|
|
207
|
+
});
|
|
208
|
+
return stamped;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The same count, given a bounded window to become true.
|
|
213
|
+
*
|
|
214
|
+
* A ONE-SHOT count taken the instant `waitForSceneToSettle` returns is the
|
|
215
|
+
* wrong instrument, and it decorrelates exactly where it matters. That helper
|
|
216
|
+
* watches `scene.children.length`, so a React world sitting in a Suspense
|
|
217
|
+
* fallback while its GLBs stream reads as "settled" with its authored tree not
|
|
218
|
+
* yet committed — every object unstamped, and the mount silently takes the
|
|
219
|
+
* structural lane for the rest of its life. Measured on racing-game, whose
|
|
220
|
+
* WARM boot took the OID lane (`r3f:<world>:<oid>` rows, 101 of 107 objects
|
|
221
|
+
* source-addressable) and whose COLD boot on the same bytes took the
|
|
222
|
+
* structural one (`ingest:0:Mesh:` rows, nothing writable) — the same game,
|
|
223
|
+
* the same source, a different instant.
|
|
224
|
+
*
|
|
225
|
+
* So the count gets a window. It returns the moment a stamp appears, which is
|
|
226
|
+
* immediately for a world that already committed; only a world with none pays
|
|
227
|
+
* the full budget, and that is the case where the answer genuinely is not
|
|
228
|
+
* knowable yet.
|
|
229
|
+
*/
|
|
230
|
+
export async function countStampedObjectsWhenCommitted(
|
|
231
|
+
scene: THREE.Object3D,
|
|
232
|
+
maxMs = 2500,
|
|
233
|
+
sleep: (ms: number) => Promise<void> = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
234
|
+
): Promise<number> {
|
|
235
|
+
const start = performance.now();
|
|
236
|
+
for (;;) {
|
|
237
|
+
const stamped = countStampedObjects(scene);
|
|
238
|
+
if (stamped > 0) return stamped;
|
|
239
|
+
if (performance.now() - start >= maxMs) return 0;
|
|
240
|
+
await sleep(100);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolve one project-relative path against a project root, POSIX-style.
|
|
246
|
+
*
|
|
247
|
+
* Spelled out rather than borrowed from `node:path` (this runs in the browser)
|
|
248
|
+
* or from `new URL(rel, 'file://…')` (which percent-encodes, so the result stops
|
|
249
|
+
* comparing equal to the raw absolute paths the OID index carries). `..` in a
|
|
250
|
+
* declared path is the ORDINARY case for a repo-vendored fixture — racing-game's
|
|
251
|
+
* root declares its world as `../../../../../../vendor/games/racing-game/src/App.tsx`
|
|
252
|
+
* — so resolving it is the whole job.
|
|
253
|
+
*/
|
|
254
|
+
export function resolveProjectPath(projectRoot: string, relative: string): string {
|
|
255
|
+
const rel = relative.replace(/\\/g, '/');
|
|
256
|
+
const base = rel.startsWith('/') ? '' : projectRoot.replace(/\\/g, '/');
|
|
257
|
+
const out: string[] = [];
|
|
258
|
+
for (const segment of `${base}/${rel}`.split('/')) {
|
|
259
|
+
if (segment === '' || segment === '.') continue;
|
|
260
|
+
if (segment === '..') out.pop();
|
|
261
|
+
else out.push(segment);
|
|
262
|
+
}
|
|
263
|
+
return `/${out.join('/')}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The directory trees THIS ingest root's own source lives under — the scope the
|
|
268
|
+
* serve-time index door (below) is allowed to answer from.
|
|
269
|
+
*
|
|
270
|
+
* It is derived from MOUNTING FACTS the manifest already states, never guessed:
|
|
271
|
+
* the project folder itself (a user's own ingested game keeps its source there)
|
|
272
|
+
* plus the parent directory of every source module the root declares
|
|
273
|
+
* (`entry`, `world.entry` — carried on the descriptor as
|
|
274
|
+
* {@link IngestGame.sourceModules}). The second is what covers a repo-vendored
|
|
275
|
+
* game, whose project folder holds only a manifest and a host shim while its
|
|
276
|
+
* actual TSX lives under `vendor/games/<id>/src/` — measured on racing-game,
|
|
277
|
+
* where all 41 stamped files sit outside the project folder.
|
|
278
|
+
*/
|
|
279
|
+
export function ingestSourceRoots(
|
|
280
|
+
projectRoot: string | null | undefined,
|
|
281
|
+
sourceModules: readonly string[] | undefined,
|
|
282
|
+
): string[] {
|
|
283
|
+
if (!projectRoot) return [];
|
|
284
|
+
const root = projectRoot.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
285
|
+
const roots = new Set<string>([root]);
|
|
286
|
+
for (const module of sourceModules ?? []) {
|
|
287
|
+
const resolved = resolveProjectPath(root, module);
|
|
288
|
+
const dir = resolved.slice(0, resolved.lastIndexOf('/'));
|
|
289
|
+
if (dir) roots.add(dir);
|
|
290
|
+
}
|
|
291
|
+
return [...roots];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* THE SERVE-TIME DOOR: did the OID transform stamp any of THIS root's own
|
|
296
|
+
* source, according to the index the dev server built while serving it?
|
|
297
|
+
*
|
|
298
|
+
* `GET /__ui-source/index` (`vite-plugin-ui-oid.ts`) records exactly what the
|
|
299
|
+
* transform stamped, and it is populated at TRANSFORM time — before the module
|
|
300
|
+
* is evaluated, and therefore long before any object exists to carry a stamp.
|
|
301
|
+
* That is precisely the ordering the graph door cannot have: on a project's
|
|
302
|
+
* first-ever cold mount (fresh Vite transform, streaming GLBs) fiber has not
|
|
303
|
+
* committed yet when the graph is counted, the count is 0, and the mount used
|
|
304
|
+
* to freeze on the structural lane for the whole session while every warm mount
|
|
305
|
+
* on the same bytes healed to the OID lane.
|
|
306
|
+
*
|
|
307
|
+
* Scoped by {@link ingestSourceRoots} rather than asking "is the index
|
|
308
|
+
* non-empty": a project can serve stamped TSX that has nothing to do with this
|
|
309
|
+
* root (a sibling `dom` root's HUD is the standing case), and answering yes for
|
|
310
|
+
* that would put a plain three.js ingest — whose creation sites really are in
|
|
311
|
+
* its own source — onto the wrong lane.
|
|
312
|
+
*/
|
|
313
|
+
export function indexStampsSourceUnder(
|
|
314
|
+
index: Readonly<Record<string, { readonly file?: unknown }>> | null | undefined,
|
|
315
|
+
sourceRoots: readonly string[],
|
|
316
|
+
): boolean {
|
|
317
|
+
if (!index || sourceRoots.length === 0) return false;
|
|
318
|
+
for (const entry of Object.values(index)) {
|
|
319
|
+
const file = entry?.file;
|
|
320
|
+
if (typeof file !== 'string') continue;
|
|
321
|
+
const clean = file.replace(/\\/g, '/');
|
|
322
|
+
if (sourceRoots.some((root) => clean === root || clean.startsWith(`${root}/`))) return true;
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* WHICH LANE, from the two doors — the only place that decision is spelled.
|
|
329
|
+
*
|
|
330
|
+
* OID lane iff EITHER door says yes; structural only on a double no.
|
|
331
|
+
*
|
|
332
|
+
* The graph door stays as the second door on purpose: a warm server restart can
|
|
333
|
+
* serve stamped bytes out of Vite's own cache without repopulating the plugin's
|
|
334
|
+
* in-memory index, and a hosted (non-dev) tier has no index endpoint at all —
|
|
335
|
+
* in both, the stamps on the live objects are the only evidence there is.
|
|
336
|
+
*/
|
|
337
|
+
export function chooseIngestAuthoringLane(doors: {
|
|
338
|
+
readonly servedStampedSource: boolean;
|
|
339
|
+
readonly graphStamps: number;
|
|
340
|
+
}): 'oid-source' | 'structural' {
|
|
341
|
+
return doors.servedStampedSource || doors.graphStamps > 0 ? 'oid-source' : 'structural';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The served OID index, or `null` when THIS SESSION'S HOST serves no
|
|
346
|
+
* `/__ui-source/*` route to ask, or the request fails. Never throws: a door
|
|
347
|
+
* that cannot answer is a `null`,
|
|
348
|
+
* and the graph door still decides.
|
|
349
|
+
*
|
|
350
|
+
* The route, not `import.meta.env.DEV`: the packaged editor serves this index
|
|
351
|
+
* from a PRODUCTION shell bundle, and asking the build flag closed the served
|
|
352
|
+
* door on exactly the tier the index exists for
|
|
353
|
+
* (`../ui-source/tier-source-write-backend.ts`).
|
|
354
|
+
*/
|
|
355
|
+
async function readServedOidIndex(): Promise<Record<string, OidEntry> | null> {
|
|
356
|
+
if (!(await serverRecordsSourceWrites())) return null;
|
|
357
|
+
try {
|
|
358
|
+
return (await createHttpSourceWriteBackend().index?.()) ?? null;
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Uncaught page errors logged during a game's BOOT WINDOW (M29).
|
|
366
|
+
*
|
|
367
|
+
* Reads the same 'runtime'-source console entries `command-listener.ts`'s
|
|
368
|
+
* `collectPageErrors` does — the window error/unhandledrejection capture
|
|
369
|
+
* `installEditorConsoleCapture` installs — fenced to this mount's own window
|
|
370
|
+
* rather than to a play run, because an ingest mount IS the run. Capped: the
|
|
371
|
+
* point is to NAME the blocker inside one sentence, not to reprint the console
|
|
372
|
+
* (the full text is there, and this list is a pointer to it).
|
|
373
|
+
*/
|
|
374
|
+
function bootWindowPageErrors(startedAt: number): string[] {
|
|
375
|
+
return editorConsole
|
|
376
|
+
.getEntries()
|
|
377
|
+
.filter(
|
|
378
|
+
(entry) =>
|
|
379
|
+
entry.level === 'error' && entry.source === 'runtime' && entry.timestamp >= startedAt,
|
|
380
|
+
)
|
|
381
|
+
.slice(-10)
|
|
382
|
+
.map((entry) => (entry.count > 1 ? `${entry.message} (×${entry.count})` : entry.message));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Wait for the game's world to be BUILT, declaration first.
|
|
387
|
+
*
|
|
388
|
+
* A game that declares `window.vgaiGame.ready` is awaited exactly once and the
|
|
389
|
+
* measured poll never runs — the game states when it is done, so the host has
|
|
390
|
+
* nothing to estimate. Everything else keeps {@link waitForSceneToSettle},
|
|
391
|
+
* which is the MEASURED fallback and is labelled as such wherever readiness is
|
|
392
|
+
* reported (`readiness.ts`).
|
|
393
|
+
*
|
|
394
|
+
* A declared signal that REJECTS does not fail the mount: the world is already
|
|
395
|
+
* captured by this point, so refusing here would throw away a live scene over a
|
|
396
|
+
* boot-completion promise. It degrades to the measured wait, LOUDLY, and the
|
|
397
|
+
* facet then reports this root as `measured` — which is the truth about which
|
|
398
|
+
* answer actually stood.
|
|
399
|
+
*/
|
|
400
|
+
async function awaitWorldReady(
|
|
401
|
+
gameId: string,
|
|
402
|
+
scene: THREE.Scene,
|
|
403
|
+
ready: (() => Promise<unknown>) | null,
|
|
404
|
+
): Promise<'declared' | 'measured'> {
|
|
405
|
+
if (ready) {
|
|
406
|
+
try {
|
|
407
|
+
await ready();
|
|
408
|
+
return 'declared';
|
|
409
|
+
} catch (err) {
|
|
410
|
+
editorConsole.error(
|
|
411
|
+
`Ingest game "${gameId}" declared \`window.vgaiGame.ready\` and it REJECTED (${String(err)}) ` +
|
|
412
|
+
'— the world was already captured, so the host fell back to the measured settle wait. ' +
|
|
413
|
+
'This root now reports readiness as measured.',
|
|
414
|
+
'ingest',
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
await waitForSceneToSettle(scene);
|
|
419
|
+
return 'measured';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Wait until the game's async world (e.g. a streamed GLTF) stops growing. */
|
|
423
|
+
async function waitForSceneToSettle(scene: THREE.Scene, maxMs = 5000): Promise<void> {
|
|
424
|
+
const start = performance.now();
|
|
425
|
+
let last = -1;
|
|
426
|
+
let stableTicks = 0;
|
|
427
|
+
return new Promise((resolve) => {
|
|
428
|
+
const tick = () => {
|
|
429
|
+
const n = scene.children.length;
|
|
430
|
+
stableTicks = n === last ? stableTicks + 1 : 0;
|
|
431
|
+
last = n;
|
|
432
|
+
if (stableTicks >= 3 || performance.now() - start > maxMs) return resolve();
|
|
433
|
+
setTimeout(tick, 120);
|
|
434
|
+
};
|
|
435
|
+
tick();
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Settle which element the host adopts into its surface, and put it there.
|
|
441
|
+
*
|
|
442
|
+
* Some games append their canvas to `document.body` — reparent it into the
|
|
443
|
+
* host. A DOM-hybrid game (React/R3F with HTML UI: HUDs, overlay panels, drei
|
|
444
|
+
* `<Html>` portals) can declare the element that OWNS its canvas via
|
|
445
|
+
* `window.vgaiGame.root` (the declared game→host contract, game-contract.ts;
|
|
446
|
+
* `__vgaiGameRoot` is the pre-contract alias). Adopting only the bare canvas
|
|
447
|
+
* would strand that UI at page level over the editor chrome, and following the
|
|
448
|
+
* canvas would require the game to know host layout internals — an arcane
|
|
449
|
+
* demand. Adopt the declared root wholesale so the game's own DOM structure
|
|
450
|
+
* survives intact; games that declare nothing keep the bare-canvas behaviour.
|
|
451
|
+
*
|
|
452
|
+
* THE PAGE IS A LEGITIMATE ANSWER, and it used to kill the mount. A game whose
|
|
453
|
+
* canvas, title wrapper, pause wrapper and game-over overlay are all direct
|
|
454
|
+
* children of the body has no wrapper div to name, so its shim declares
|
|
455
|
+
* `root: document.body` and is right to. The
|
|
456
|
+
* page is an ANCESTOR of our surface, so adopting it is not merely wrong, it
|
|
457
|
+
* throws — `hostEl.appendChild(document.body)` → "The new child element
|
|
458
|
+
* contains the parent" — and the whole mount died there. In-realm, that
|
|
459
|
+
* declaration means "everything I appended to the page", and everything the
|
|
460
|
+
* game appended to the page is already inside the surface, because the realm
|
|
461
|
+
* put it there (game-realm-page.ts). So the surface IS the adopted root, and
|
|
462
|
+
* there is nothing left to reparent or restyle: the host already owns its box.
|
|
463
|
+
*
|
|
464
|
+
* Otherwise the pane sizes the adopted element from here on, so strip whatever
|
|
465
|
+
* page-level positioning the game used while it owned the whole window (S-4:
|
|
466
|
+
* this runs for the BARE-CANVAS case too, which is the case it was missing — a
|
|
467
|
+
* game that declares no root hands us a canvas three already stamped with
|
|
468
|
+
* literal `${w}px`/`${h}px`, and the host's own resizes use
|
|
469
|
+
* `updateStyle:false`, so nothing else would ever write that stamp again).
|
|
470
|
+
*/
|
|
471
|
+
function adoptGameDomRoot(hostEl: HTMLElement, surface: HTMLElement): HTMLElement {
|
|
472
|
+
const realmWindow = ingestGameRealmWindow();
|
|
473
|
+
const declaredRoot =
|
|
474
|
+
readIngestGameContract()?.root ??
|
|
475
|
+
(realmWindow as unknown as { __vgaiGameRoot?: unknown }).__vgaiGameRoot;
|
|
476
|
+
const declared =
|
|
477
|
+
declaredRoot instanceof HTMLElement && declaredRoot.contains(surface) ? declaredRoot : surface;
|
|
478
|
+
if (declared.contains(hostEl)) return hostEl;
|
|
479
|
+
// Already nested INSIDE the game's own DOM under the host (a staged page's
|
|
480
|
+
// renderer lives inside its own `#container`, among siblings whose paint
|
|
481
|
+
// order the page designed): the page owns its layout — hoisting the element
|
|
482
|
+
// to the host's end re-stacks it OVER later siblings (measured on
|
|
483
|
+
// css3d_periodictable: the renderer covered the page's own `#menu`, so its
|
|
484
|
+
// TABLE/SPHERE buttons never received a click). Only a DIRECT child gets
|
|
485
|
+
// the host's box claim, and only a DETACHED/outside element is rescued in.
|
|
486
|
+
if (declared.parentElement !== hostEl && hostEl.contains(declared)) return declared;
|
|
487
|
+
claimHostSurfaceBox(declared);
|
|
488
|
+
if (declared.parentElement !== hostEl) hostEl.appendChild(declared);
|
|
489
|
+
return declared;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Mount an unmodified game and capture its live scene as a `MountedThreeRoot`.
|
|
494
|
+
* `store` is needed by the authoring adapter for shared selection state; the
|
|
495
|
+
* captured objects get stable structural-path ids in the live authoring
|
|
496
|
+
* adapter's projection (never a fabricated descriptor and never a foreign
|
|
497
|
+
* `userData` mutation).
|
|
498
|
+
*/
|
|
499
|
+
export async function mountIngestGame(
|
|
500
|
+
store: EditorShellStore,
|
|
501
|
+
game: IngestGame,
|
|
502
|
+
gameContainer: HTMLElement,
|
|
503
|
+
opts: MountIngestOptions = {},
|
|
504
|
+
): Promise<IngestMount> {
|
|
505
|
+
// The upstream games expect an element with id="container" to mount into.
|
|
506
|
+
const hostEl = document.createElement('div');
|
|
507
|
+
hostEl.id = 'container';
|
|
508
|
+
hostEl.style.cssText = `position:absolute;inset:0;width:100%;height:100%;overflow:hidden;${GAME_SURFACE_CONTAINMENT_CSS}`;
|
|
509
|
+
gameContainer.appendChild(hostEl);
|
|
510
|
+
|
|
511
|
+
// THE GAME'S PAGE IS THIS BOX, from before its first module runs. A game
|
|
512
|
+
// written to own a tab appends its canvas and every overlay to
|
|
513
|
+
// `document.body`; the realm hands it this element instead, and the
|
|
514
|
+
// containment CSS above makes the element the containing block for the
|
|
515
|
+
// `position:fixed` overlays that would otherwise paint over editor chrome.
|
|
516
|
+
// Host policy, not per-game CSS — see `game-realm-page.ts` for the four
|
|
517
|
+
// page-shaped assumptions this covers and why each one breaks an in-realm
|
|
518
|
+
// mount. Registered on the DEFAULT realm because an ingest descriptor's
|
|
519
|
+
// module urls carry no `?vgai-mount=` (binding-resolver.ts's
|
|
520
|
+
// `resolveIngestDescriptor` builds bare `fsImportPath` urls).
|
|
521
|
+
const realmPage = setGameSurface(hostEl);
|
|
522
|
+
|
|
523
|
+
// THE GAME'S OWN PAGE STYLESHEET, contained to this box. `hostEl` IS the
|
|
524
|
+
// game's page in-realm, so it is exactly the right scope root: the sheet's
|
|
525
|
+
// `html`/`body` rules land on it, everything else lands on what the game
|
|
526
|
+
// appends inside it, and the editor's document sees none of it
|
|
527
|
+
// (`scoped-game-css.ts` — the rules are served inside `@scope`). Awaited
|
|
528
|
+
// BEFORE the game's entry runs so the HUD's first paint is already styled.
|
|
529
|
+
// A project that declares no stylesheet mounts exactly as it did before.
|
|
530
|
+
markGameCssScope(hostEl);
|
|
531
|
+
const scopedCssProject = getCurrentProject();
|
|
532
|
+
if (scopedCssProject) await ensureScopedGameStyles(scopedCssProject.rootPath);
|
|
533
|
+
|
|
534
|
+
// DOM-shim: stub the elements a DOM-dependent game expects (e.g. #blocker),
|
|
535
|
+
// so its getElementById calls succeed without editing the game. A stub is
|
|
536
|
+
// MARKED so the served-bundle boot can retire it when the game's own staged
|
|
537
|
+
// page supplies that id (`served-html-boot.ts`): stubs are made before the
|
|
538
|
+
// declared DOM is staged, so this existence check cannot see the real
|
|
539
|
+
// element yet, and a stub left in place shadows it — measured on
|
|
540
|
+
// css3d_periodictable, whose TABLE/SPHERE buttons bound their listeners to
|
|
541
|
+
// invisible stubs while the visible buttons did nothing.
|
|
542
|
+
const stubEls: HTMLElement[] = [];
|
|
543
|
+
for (const id of game.domStubs ?? []) {
|
|
544
|
+
if (document.getElementById(id)) continue;
|
|
545
|
+
const stub = document.createElement('div');
|
|
546
|
+
stub.id = id;
|
|
547
|
+
stub.style.display = 'none';
|
|
548
|
+
stub.dataset[DOM_STUB_MARK] = '';
|
|
549
|
+
hostEl.appendChild(stub);
|
|
550
|
+
stubEls.push(stub);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// THE `three` THE GAME'S OWN MODULES WILL RESOLVE — and therefore the ONE
|
|
554
|
+
// whose `WebGLRenderer.prototype` is worth trapping. Under the packaged
|
|
555
|
+
// runtime the game's entry is served as `/@fs/<project>/…` by the
|
|
556
|
+
// PROJECT-rooted Vite (the only Vite there — the editor's shell is a
|
|
557
|
+
// prebuilt static bundle with its own `three` inlined), so a shell namespace
|
|
558
|
+
// traps a class nobody constructs. MEASURED on a packaged build against a
|
|
559
|
+
// ~30-line unmodified three.js game: three.js's own "Multiple instances of
|
|
560
|
+
// Three.js being imported" warning, then the mount failing by name 15s later
|
|
561
|
+
// with "the game never rendered, or it bundles its own (un-shared) copy of
|
|
562
|
+
// three". Resolved ONCE here and handed to everything below; see
|
|
563
|
+
// `../three-ingest-runtime.ts`. Under dev/hosted there is one graph
|
|
564
|
+
// and this is byte-identical to the static imports it replaces.
|
|
565
|
+
const runtime = await resolveThreeIngestRuntimeForEditor();
|
|
566
|
+
const projectThree = runtime.three;
|
|
567
|
+
|
|
568
|
+
// 1. Trap the shared three's renderer (the game's `import 'three'` resolves here).
|
|
569
|
+
// Also pass the shared `EffectComposer` addon ctor so a
|
|
570
|
+
// game's own composer is collected too — resize wiring below (D-C3).
|
|
571
|
+
// `isHostRenderer` keeps the EDITOR's own drawing out of the capture: the
|
|
572
|
+
// trap is on the shared prototype, so our renders reach it too — including
|
|
573
|
+
// the offscreen thumbnail/preview renderers the editor builds on demand.
|
|
574
|
+
const capture: SceneCaptureHandle = installSceneCapture(projectThree, runtime.EffectComposer, {
|
|
575
|
+
isHostRenderer,
|
|
576
|
+
// CSS3DRenderer is a shared Three addon exactly like EffectComposer: the
|
|
577
|
+
// host and an unmodified `three/addons/renderers/CSS3DRenderer.js` import
|
|
578
|
+
// resolve to this one class. Its prototype render carries the same scene +
|
|
579
|
+
// camera pair even though it never touches WebGLRenderer.
|
|
580
|
+
additionalRendererCtors: [runtime.CSS3DRenderer],
|
|
581
|
+
// ZERO INFERENCE, first leg: the game's own contract may name its world, in
|
|
582
|
+
// which case first-render-wins never runs. Read LAZILY — the contract is
|
|
583
|
+
// declared by the game's own modules, which have not executed yet at this
|
|
584
|
+
// line. A malformed declaration is refused by name below rather than
|
|
585
|
+
// silently falling back, so a wrong `world` is attributable.
|
|
586
|
+
declaredScene: () => {
|
|
587
|
+
const reading = readGameWorld(readIngestGameContract());
|
|
588
|
+
if (reading.malformed !== null) {
|
|
589
|
+
editorConsole.error(
|
|
590
|
+
`Ingest game "${game.id}" ${reading.malformed} — falling back to the MEASURED ` +
|
|
591
|
+
'first-render adoption.',
|
|
592
|
+
'ingest',
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
return reading.world;
|
|
596
|
+
},
|
|
597
|
+
// ZERO INFERENCE, second leg: whichever way the world was adopted, the
|
|
598
|
+
// provenance and every later distinct world become a recorded fact
|
|
599
|
+
// (`world-adoption.ts` → the `worldAdoption` facet), never a silent
|
|
600
|
+
// permanent commitment.
|
|
601
|
+
onWorldAdoption: worldAdoptionRecorder(game.id),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// 2. Asset-path adapter: redirect the game's relative asset paths to the served files.
|
|
605
|
+
if (game.assets) {
|
|
606
|
+
const map = game.assets;
|
|
607
|
+
projectThree.DefaultLoadingManager.setURLModifier((url) => {
|
|
608
|
+
for (const key of Object.keys(map)) if (url.includes(key)) return map[key]!;
|
|
609
|
+
return url;
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 3. Run the unmodified game; the trap captures on its first rendered frame.
|
|
614
|
+
// Signal COLD MOUNT before the entry executes: editor mounts are for
|
|
615
|
+
// inspection first, so a session-driven game MAY defer its side-effects
|
|
616
|
+
// (backend connection, narrative, audio) until the editor's play control
|
|
617
|
+
// dispatches 'vgai:ingest-play' (getIngestPlayControl, ingest/mount-ingest-root.ts).
|
|
618
|
+
// Rendering must continue regardless — capture needs a frame — and games
|
|
619
|
+
// that ignore the flag behave exactly as before (opt-in, never demanded).
|
|
620
|
+
(window as unknown as { __vgaiMountCold?: boolean }).__vgaiMountCold = true;
|
|
621
|
+
// THE BOOT WINDOW OPENS HERE (M29). Everything the page throws from this
|
|
622
|
+
// instant until the mount resolves belongs to this game's boot, and it is the
|
|
623
|
+
// fact that separates "crashed before ready" from "never became ready" — the
|
|
624
|
+
// two the old single sentence could not tell apart.
|
|
625
|
+
const bootWindowStartedAt = Date.now();
|
|
626
|
+
await game.load();
|
|
627
|
+
const timeoutMs = opts.captureTimeoutMs ?? 10_000;
|
|
628
|
+
// WHO ANSWERS "is it ready" — read once, right after the game's own modules
|
|
629
|
+
// have run and therefore after its contract exists. A declaration makes this
|
|
630
|
+
// root's readiness `declared`; its absence leaves the MEASURED waits below
|
|
631
|
+
// standing, and says so in the facet rather than silently.
|
|
632
|
+
const declaredReady = readGameReady(readIngestGameContract());
|
|
633
|
+
if (declaredReady.malformed !== null) {
|
|
634
|
+
editorConsole.error(
|
|
635
|
+
`Ingest game "${game.id}" ${declaredReady.malformed} — the host is measuring readiness ` +
|
|
636
|
+
'instead.',
|
|
637
|
+
'ingest',
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
const readinessSource = declaredReady.ready === null ? 'measured' : 'declared';
|
|
641
|
+
recordRootReadiness({
|
|
642
|
+
rootId: game.id,
|
|
643
|
+
mechanism: declaredReady.ready === null ? 'measured-wait' : 'contract-ready',
|
|
644
|
+
source: readinessSource,
|
|
645
|
+
state: 'waiting',
|
|
646
|
+
});
|
|
647
|
+
let rt: Awaited<ReturnType<SceneCaptureHandle['waitForCapture']>>;
|
|
648
|
+
try {
|
|
649
|
+
// The window is VISIBLE time (see `visible-capture-window.ts`): a tab that
|
|
650
|
+
// boots in the background parks here instead of dying, with the trap still
|
|
651
|
+
// installed, so the first frame after the human foregrounds the tab is the
|
|
652
|
+
// captured one. `setCaptureWait` is what keeps that park from reading as a
|
|
653
|
+
// hung mount — `vgai status` names it.
|
|
654
|
+
rt = await capture.waitForCapture({
|
|
655
|
+
timeoutMs,
|
|
656
|
+
onWait: (wait) => setCaptureWait(game.id, wait),
|
|
657
|
+
});
|
|
658
|
+
} catch (err) {
|
|
659
|
+
// Nothing was captured: the game's `three` never reached the host trap, so
|
|
660
|
+
// there is no live scene to author. Tear the host state down and fail by
|
|
661
|
+
// name.
|
|
662
|
+
capture.uninstall();
|
|
663
|
+
projectThree.DefaultLoadingManager.setURLModifier((u) => u);
|
|
664
|
+
for (const el of stubEls) el.remove();
|
|
665
|
+
clearGameSurface();
|
|
666
|
+
hostEl.remove();
|
|
667
|
+
clearWorldAdoption(game.id);
|
|
668
|
+
// M29 — THREE BLOCKERS, THREE SENTENCES. The old single line said "rendered
|
|
669
|
+
// no capturable frame within timeout" for all of them, which pointed a
|
|
670
|
+
// reader at the host's wait when the actual blocker was the game's own
|
|
671
|
+
// async-init throw (it misdirected two sweep measurements). The choice is
|
|
672
|
+
// made from facts already in hand: did anything throw during the boot
|
|
673
|
+
// window, and did the game declare readiness at all.
|
|
674
|
+
const description = describeMountFailure({
|
|
675
|
+
gameId: game.id,
|
|
676
|
+
timeoutMs,
|
|
677
|
+
readinessSource,
|
|
678
|
+
pageErrors: bootWindowPageErrors(bootWindowStartedAt),
|
|
679
|
+
cause: String(err),
|
|
680
|
+
});
|
|
681
|
+
recordRootReadiness({
|
|
682
|
+
rootId: game.id,
|
|
683
|
+
mechanism: declaredReady.ready === null ? 'measured-wait' : 'contract-ready',
|
|
684
|
+
source: readinessSource,
|
|
685
|
+
state: 'failed',
|
|
686
|
+
failure: description.kind,
|
|
687
|
+
});
|
|
688
|
+
throw new Error(description.message);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const gameDomRoot = adoptGameDomRoot(hostEl, rt.renderer.domElement);
|
|
692
|
+
|
|
693
|
+
// WHICH CANVAS IS THE GAME'S PICTURE — declaration first. The contract's
|
|
694
|
+
// `presentation` is the game's own statement; absent it, the captured
|
|
695
|
+
// renderer's element is the host's MEASUREMENT (a real read off the render
|
|
696
|
+
// trap, not the DOM-order guess every late reader used to make). Both are
|
|
697
|
+
// recorded with their provenance, which is what lets the screenshot,
|
|
698
|
+
// staleness and canvas-medium readers stop sniffing (`presentation-surface.ts`).
|
|
699
|
+
const declaredPresentation = readGamePresentation(readIngestGameContract());
|
|
700
|
+
if (declaredPresentation.malformed !== null) {
|
|
701
|
+
editorConsole.error(
|
|
702
|
+
`Ingest game "${game.id}" ${declaredPresentation.malformed} — the host is using the ` +
|
|
703
|
+
'captured renderer’s canvas instead.',
|
|
704
|
+
'ingest',
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
recordPresentationSurface(
|
|
708
|
+
game.id,
|
|
709
|
+
declaredPresentation.canvas ?? rt.renderer.domElement,
|
|
710
|
+
declaredPresentation.canvas ? 'declared' : 'measured',
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
// Pointer lock: a first-person game asks for it on its OWN element (a click
|
|
714
|
+
// handler on its canvas), which no
|
|
715
|
+
// window/document proxy can see. The gate is installed on the element
|
|
716
|
+
// method itself and scoped to registered game surfaces, so the editor
|
|
717
|
+
// viewport's own pointer lock is untouched.
|
|
718
|
+
installGamePointerLockGate();
|
|
719
|
+
|
|
720
|
+
// 4. Let the game finish building its world — its own `ready` when it
|
|
721
|
+
// declares one, the measured settle poll otherwise — then build the
|
|
722
|
+
// authoring adapter.
|
|
723
|
+
const readinessStood = await awaitWorldReady(game.id, rt.scene, declaredReady.ready);
|
|
724
|
+
recordRootReadiness({
|
|
725
|
+
rootId: game.id,
|
|
726
|
+
mechanism: readinessStood === 'declared' ? 'contract-ready' : 'measured-wait',
|
|
727
|
+
source: readinessStood,
|
|
728
|
+
state: 'ready',
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// Loop gate (begin/endEdit): the scene-capture trap recorded the game's
|
|
732
|
+
// setAnimationLoop callback, so we freeze the game's OWN loop (set null) and
|
|
733
|
+
// resume it (re-set the recorded callback) for stable editing — even of dynamic
|
|
734
|
+
// objects. (Games driving their loop via raw requestAnimationFrame instead of
|
|
735
|
+
// setAnimationLoop have no recorded callback → pause is a no-op for them.)
|
|
736
|
+
const loop = {
|
|
737
|
+
pause: () => rt.renderer.setAnimationLoop?.(null),
|
|
738
|
+
resume: () => {
|
|
739
|
+
const cb = capture.getAnimationLoop(rt.renderer);
|
|
740
|
+
if (cb) rt.renderer.setAnimationLoop?.(cb);
|
|
741
|
+
},
|
|
742
|
+
};
|
|
743
|
+
// The game's own DATA writer, if it declares one — registered for EVERY
|
|
744
|
+
// ingest mount, including as `null`, because setting it is also what takes
|
|
745
|
+
// the previous mount's writer back down (see `ingest-data-writer.ts`
|
|
746
|
+
// §RESOURCE OWNERSHIP). It is only a loader here; nothing imports it until an
|
|
747
|
+
// edit needs it.
|
|
748
|
+
setIngestDataWriter(game.dataWriter ?? null);
|
|
749
|
+
|
|
750
|
+
// WHICH AUTHORING LANE — decided by MEASUREMENT, never by the game's id.
|
|
751
|
+
//
|
|
752
|
+
// A game whose own source the serve-time OID transform reached carries that
|
|
753
|
+
// stamp on the objects fiber constructed from it (`userData.oid`; the
|
|
754
|
+
// transform's R3F dialect emits `userData-oid`, which fiber pierces onto the
|
|
755
|
+
// object). For such a world the JSX callsite IS the object's source address,
|
|
756
|
+
// and the creation-site registry the structural lane writes through is
|
|
757
|
+
// structurally EMPTY — every `new` expression that built it lives in
|
|
758
|
+
// `node_modules`.
|
|
759
|
+
//
|
|
760
|
+
// TWO DOORS, and the SERVE-TIME one is asked first because it is already true
|
|
761
|
+
// at this instant on the coldest boot: the transform populates the index
|
|
762
|
+
// before the module it stamped is even evaluated, while the graph can only
|
|
763
|
+
// answer once fiber has committed. The graph door remains, for the case
|
|
764
|
+
// where no index exists to read (a warm server restart serving cached
|
|
765
|
+
// bytes). Structural only when BOTH say no — see
|
|
766
|
+
// `chooseIngestAuthoringLane`.
|
|
767
|
+
const sourceRoots = ingestSourceRoots(getCurrentProject()?.rootPath, game.sourceModules);
|
|
768
|
+
const servedStampedSource = indexStampsSourceUnder(await readServedOidIndex(), sourceRoots);
|
|
769
|
+
// The graph poll is the SECOND door, so it is only worth paying for when the
|
|
770
|
+
// first said no; a yes has already decided the lane.
|
|
771
|
+
const graphStamps = servedStampedSource ? 0 : await countStampedObjectsWhenCommitted(rt.scene);
|
|
772
|
+
const authoring =
|
|
773
|
+
chooseIngestAuthoringLane({ servedStampedSource, graphStamps }) === 'oid-source'
|
|
774
|
+
? oidSourceThree(store, rt.scene, {
|
|
775
|
+
worldId: game.id,
|
|
776
|
+
loop,
|
|
777
|
+
camera: rt.camera,
|
|
778
|
+
journal: authoringJournal(game.id),
|
|
779
|
+
})
|
|
780
|
+
: structuralThree(store, rt.scene, {
|
|
781
|
+
loop,
|
|
782
|
+
camera: rt.camera,
|
|
783
|
+
journal: authoringJournal(game.id),
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// D10/T7.6: the game-level play-state control surface (`Game.play.pause()`)
|
|
787
|
+
// calls THIS `setPaused` — separately from the authoring adapter's own
|
|
788
|
+
// edit-time freeze/unfreeze (which calls `loop.pause()`/`loop.resume()`
|
|
789
|
+
// directly, above), so a gizmo drag never trips the loop-gate honesty
|
|
790
|
+
// report `createIngestLoopGate` makes for an actual play/pause request.
|
|
791
|
+
// S-5: hand the in-page mount the SAME-REALM scheduling gate, so pausing
|
|
792
|
+
// freezes the game's `setInterval`/`setTimeout`/raw-rAF drivers too and the
|
|
793
|
+
// verdict is MEASURED rather than declared.
|
|
794
|
+
const loopGate = createIngestLoopGate(capture, rt.renderer, game.id, loop, {
|
|
795
|
+
realmGate: gameLoopGate(),
|
|
796
|
+
// The probe is ASYNC (it watches a real-time window), so the verdict lands
|
|
797
|
+
// well after `setPaused` returned. Nothing else would notify the store, and
|
|
798
|
+
// `collectState` is push-based — the control API would keep serving a
|
|
799
|
+
// snapshot taken before the measurement existed, reporting `loop: null`
|
|
800
|
+
// forever. Broadcast so `vgai status` sees the verdict it just produced.
|
|
801
|
+
onVerdict: () => store.notifyIngestEdit(),
|
|
802
|
+
});
|
|
803
|
+
// The game's DECLARED system surface (`window.vgaiGame.systems`), projected
|
|
804
|
+
// onto the host's ordinary `SystemAdapters.debug`. `activateCapturedThreeIngest`
|
|
805
|
+
// hands `mounted.systems` straight to `setActiveSystems`, so a shim that
|
|
806
|
+
// declares verbs makes `game.commands()`/`game.state()` work through the same
|
|
807
|
+
// bridge path first-party content uses — no ingest-specific door. A game that
|
|
808
|
+
// declares nothing yields `null` here and keeps the pre-contract floor.
|
|
809
|
+
const { systems, renderDebug, debugCollisions } = wireIngestSystems({
|
|
810
|
+
contractSystems: readIngestGameContract()?.systems,
|
|
811
|
+
surface: 'three',
|
|
812
|
+
runtime: rt,
|
|
813
|
+
setRenderPassHooks: (hooks) => capture.setRenderPassHooks(hooks),
|
|
814
|
+
});
|
|
815
|
+
// A name two feeders both declare is REFUSED when called, by a coded error
|
|
816
|
+
// naming both. Said here too — same as the canvas mount — because a refusal
|
|
817
|
+
// nobody sees until they call the name is a defect that ships.
|
|
818
|
+
for (const collision of debugCollisions) {
|
|
819
|
+
editorConsole.error(`Ingest game "${game.id}" — ${collision}; it will refuse`, 'ingest');
|
|
820
|
+
}
|
|
821
|
+
const mounted: MountedThreeRoot = {
|
|
822
|
+
kind: 'three',
|
|
823
|
+
scene: rt.scene,
|
|
824
|
+
camera: rt.camera,
|
|
825
|
+
drivesOwnLoop: true,
|
|
826
|
+
...(Object.keys(systems).length > 0 ? { systems } : {}),
|
|
827
|
+
setPaused: (paused) => loopGate.setPaused(paused),
|
|
828
|
+
// Also resize any composer the game renders through, so
|
|
829
|
+
// its own (intentionally downsampled, e.g. `UnrealBloomPass`) render
|
|
830
|
+
// targets track the host pane instead of staying stale at mount size.
|
|
831
|
+
resize: (w, h) => {
|
|
832
|
+
// WHO OWNS THE PICTURE'S SHAPE. A game that registered its own `resize`
|
|
833
|
+
// listener has a size policy — letterboxing to a fixed aspect is the
|
|
834
|
+
// common one — and the realm now reports the
|
|
835
|
+
// PANE as `window.innerWidth`/`innerHeight`, so its own handler produces
|
|
836
|
+
// the right answer for the pane. Forcing `setSize(pane)` on top of that
|
|
837
|
+
// would stretch exactly the game that took the trouble to letterbox
|
|
838
|
+
// itself. So: tell the game, and let it size its renderer.
|
|
839
|
+
if (realmPage.gameResizesItself) {
|
|
840
|
+
realmPage.dispatchResize();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
// Otherwise the host owns it, exactly as before.
|
|
844
|
+
const size = hostSurfaceBackingSize(w, h);
|
|
845
|
+
rt.renderer.setSize(size.width, size.height, false);
|
|
846
|
+
capture.resizeComposers(size.width, size.height);
|
|
847
|
+
// S-4: re-assert the host's CSS box. `setSize(…, false)` deliberately
|
|
848
|
+
// never touches style, so this is the only thing that survives a game's
|
|
849
|
+
// own window-resize handler re-stamping pixels.
|
|
850
|
+
claimHostSurfaceBox(gameDomRoot);
|
|
851
|
+
},
|
|
852
|
+
dispose: () => {
|
|
853
|
+
// Drop the render-pass bracket and reject any armed capture BEFORE the
|
|
854
|
+
// renderer goes, so no wrapper outlives the mount on the game's objects.
|
|
855
|
+
capture.setRenderPassHooks(null);
|
|
856
|
+
// The bracket's second consumer, released through its own ONE teardown
|
|
857
|
+
// path — a screenshot waiting on a frame settles now rather than hanging
|
|
858
|
+
// out its timeout over a mount that is gone.
|
|
859
|
+
clearIngestFrameSource();
|
|
860
|
+
renderDebug?.dispose();
|
|
861
|
+
try {
|
|
862
|
+
rt.renderer.setAnimationLoop?.(null);
|
|
863
|
+
rt.renderer.dispose?.();
|
|
864
|
+
} catch {
|
|
865
|
+
/* ignore */
|
|
866
|
+
}
|
|
867
|
+
capture.uninstall();
|
|
868
|
+
projectThree.DefaultLoadingManager.setURLModifier((url) => url);
|
|
869
|
+
for (const el of stubEls) el.remove();
|
|
870
|
+
// A game that held pointer lock must not keep it past its own mount, and
|
|
871
|
+
// the realm must stop standing in for a surface that no longer exists.
|
|
872
|
+
releaseGamePointerLock();
|
|
873
|
+
clearGameSurface();
|
|
874
|
+
// RESOURCE OWNERSHIP: this mount recorded these three per-root facts, so
|
|
875
|
+
// this mount's ONE teardown path drops them. Per-root, never a blanket
|
|
876
|
+
// clear — a composite mounts roots independently and a sibling's answers
|
|
877
|
+
// are still true (the rule `clearMountFailureReport` already follows).
|
|
878
|
+
clearPresentationSurface(game.id);
|
|
879
|
+
clearRootReadiness(game.id);
|
|
880
|
+
clearWorldAdoption(game.id);
|
|
881
|
+
// Removing the surface removes the game's ENTIRE DOM with it — canvas,
|
|
882
|
+
// title wrapper, pause wrapper, game-over overlay — because the realm
|
|
883
|
+
// put every one of them inside it (game-realm-page.ts, reason 1).
|
|
884
|
+
hostEl.remove();
|
|
885
|
+
},
|
|
886
|
+
authoring,
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
return {
|
|
890
|
+
game: mounted,
|
|
891
|
+
authoring,
|
|
892
|
+
hostEl,
|
|
893
|
+
capture,
|
|
894
|
+
scene: rt.scene,
|
|
895
|
+
realmLoopVerdict: () => loopGate.lastVerdict(),
|
|
896
|
+
};
|
|
897
|
+
}
|