@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,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booting a served bundle THE WAY ITS OWN DOCUMENT SAYS TO.
|
|
3
|
+
*
|
|
4
|
+
* Every other ingest entry is an ES module and the host imports it. A bundle
|
|
5
|
+
* built by a CLASSIC-SCRIPT toolchain is not one, and importing it as a module
|
|
6
|
+
* is not a degraded boot — it is a different language mode. Measured on
|
|
7
|
+
* `public/ingest/cuberun` (Create React App / webpack 4, the recorded build
|
|
8
|
+
* recipe in `scripts/cuberun/steps/01-build.mjs`):
|
|
9
|
+
*
|
|
10
|
+
* TypeError: Cannot read properties of undefined (reading 'webpackJsonpcubeworld')
|
|
11
|
+
*
|
|
12
|
+
* because the chunk opens `(this.webpackJsonpcubeworld = this.webpackJsonpcubeworld || [])`
|
|
13
|
+
* and top-level `this` is the window in a classic script and `undefined` in an
|
|
14
|
+
* ES module. Two more facts come with it: the entry is the LAST of three
|
|
15
|
+
* scripts (webpack runtime, vendor chunk, app chunk) that must run in order,
|
|
16
|
+
* and the app mounts into a `<div id="root">` its own document declares.
|
|
17
|
+
*
|
|
18
|
+
* ZERO INFERENCE about any of that. `index.html` beside the bundle is the
|
|
19
|
+
* game's OWN statement of its boot: which scripts, in what order, in which
|
|
20
|
+
* language mode, and what DOM exists before they run. This module reads that
|
|
21
|
+
* statement and does what it says; it never sniffs the source, and a bundle
|
|
22
|
+
* whose entry the document loads with `type="module"` (tanks, simcity, every
|
|
23
|
+
* three.js example here) takes the ordinary module path untouched.
|
|
24
|
+
*
|
|
25
|
+
* WHAT THIS DOES NOT DO, because the host already does it:
|
|
26
|
+
* - SIZE/CONTAIN the page. `mountIngestGame` makes `hostEl` the realm's
|
|
27
|
+
* `document.body` (`setGameSurface`), absolutely positioned and full-size,
|
|
28
|
+
* so the transplanted body lands in a real box.
|
|
29
|
+
* - STYLE it. The root's `styles` manifest field serves the bundle's own
|
|
30
|
+
* stylesheet rewritten into `@scope` (`server/scoped-game-css.ts`), which is
|
|
31
|
+
* what turns `#root,body,html{width:100%;height:100%}` into a sized root
|
|
32
|
+
* without the editor seeing one of those rules. A `<link rel=stylesheet>` in
|
|
33
|
+
* the document is NOT auto-loaded here: an unscoped page sheet restyles the
|
|
34
|
+
* editor, and the declaration is the mechanism that already exists for it.
|
|
35
|
+
* - SHADOW globals. The concatenated source is prefixed with
|
|
36
|
+
* `GAME_GLOBALS_PRELUDE`, whose own doc comment records that it is kept
|
|
37
|
+
* evaluable outside ES-module scope precisely so a non-module evaluation
|
|
38
|
+
* path like this one can use it.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { loadGameCssScoper, SCOPED_STYLE_MARK } from '../host/game-realm-page';
|
|
42
|
+
import { GAME_GLOBALS_PRELUDE, gameRealmPage } from '../host/gated-globals';
|
|
43
|
+
import { ingestGameRealmWindow } from './game-contract-realm';
|
|
44
|
+
|
|
45
|
+
/** One `<script>` the document declares, in document order. */
|
|
46
|
+
export interface DeclaredScript {
|
|
47
|
+
/** Absolute served URL, or `null` for an inline script. */
|
|
48
|
+
readonly src: string | null;
|
|
49
|
+
/** Inline body; empty for a `src` script. */
|
|
50
|
+
readonly inline: string;
|
|
51
|
+
/** `true` when the document loads it with `type="module"`. */
|
|
52
|
+
readonly module: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What a served bundle's own `index.html` declares about its boot. */
|
|
56
|
+
export interface DeclaredBoot {
|
|
57
|
+
/** The document's own inline `<style>` texts, in order — installed SCOPED. */
|
|
58
|
+
readonly inlineCss: readonly string[];
|
|
59
|
+
readonly scripts: readonly DeclaredScript[];
|
|
60
|
+
/** The document's body markup with every `<script>` removed. */
|
|
61
|
+
readonly bodyHtml: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse a served bundle's `index.html`. `null` when the folder ships none —
|
|
66
|
+
* an ordinary case (a bundle built as a bare module has nothing to declare),
|
|
67
|
+
* and never an error.
|
|
68
|
+
*/
|
|
69
|
+
export async function readDeclaredBoot(
|
|
70
|
+
baseUrl: string,
|
|
71
|
+
fetchImpl: typeof fetch = globalThis.fetch,
|
|
72
|
+
): Promise<DeclaredBoot | null> {
|
|
73
|
+
let html: string;
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetchImpl(`${baseUrl}index.html`);
|
|
76
|
+
if (!response.ok) return null;
|
|
77
|
+
html = await response.text();
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
|
82
|
+
// The dev server answers an unknown path with the EDITOR's index.html, which
|
|
83
|
+
// parses perfectly well and would then be read as this game's boot. A served
|
|
84
|
+
// bundle's own document is the one whose scripts resolve under its own base.
|
|
85
|
+
// The page's own inline <style> blocks are part of its statement too — a
|
|
86
|
+
// game may size its mount div there (phaser-catch-the-cat's
|
|
87
|
+
// `#catch-the-cat { width: 100% }`). They are collected for SCOPED
|
|
88
|
+
// installation (the same `@scope` transform declared sheets get), never
|
|
89
|
+
// injected raw: an unscoped page rule would restyle the editor.
|
|
90
|
+
const inlineCss: string[] = [];
|
|
91
|
+
for (const el of Array.from(parsed.querySelectorAll('style'))) {
|
|
92
|
+
const text = el.textContent ?? '';
|
|
93
|
+
if (text.trim().length > 0) inlineCss.push(text);
|
|
94
|
+
el.remove();
|
|
95
|
+
}
|
|
96
|
+
const scripts: DeclaredScript[] = [];
|
|
97
|
+
for (const el of Array.from(parsed.querySelectorAll('script'))) {
|
|
98
|
+
// A `<script>` with a non-JS `type` is DATA the game reads back out of
|
|
99
|
+
// the DOM (`x-shader/x-vertex` blocks fetched by id — three.js example
|
|
100
|
+
// convention), never code. It must SURVIVE into the staged DOM, not be
|
|
101
|
+
// queued for evaluation: concatenating GLSL as JS is a SyntaxError, and
|
|
102
|
+
// stripping it left `getElementById('vertexshader')` null (measured:
|
|
103
|
+
// three-points-waves' TypeError, 2026-08-27). Head-
|
|
104
|
+
// declared data tags are moved into the body so the transplant carries
|
|
105
|
+
// them.
|
|
106
|
+
const type = el.getAttribute('type');
|
|
107
|
+
const executable =
|
|
108
|
+
type === null || type === 'module' || /^(text|application)\/javascript$/i.test(type);
|
|
109
|
+
if (!executable) {
|
|
110
|
+
if (!parsed.body.contains(el)) parsed.body.prepend(el);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const rawSrc = el.getAttribute('src');
|
|
114
|
+
// A cross-origin script keeps its FULL href: flattening a CDN URL to its
|
|
115
|
+
// pathname re-rooted it onto this origin, where the SPA fallback answered
|
|
116
|
+
// with HTML (server-survival's three.min.js CDN dependency, 2026-08-27).
|
|
117
|
+
const resolved =
|
|
118
|
+
rawSrc === null ? null : new URL(rawSrc, `${globalThis.location.origin}${baseUrl}`);
|
|
119
|
+
scripts.push({
|
|
120
|
+
src:
|
|
121
|
+
resolved === null
|
|
122
|
+
? null
|
|
123
|
+
: resolved.origin === globalThis.location.origin
|
|
124
|
+
? resolved.pathname
|
|
125
|
+
: resolved.href,
|
|
126
|
+
inline: rawSrc === null ? (el.textContent ?? '') : '',
|
|
127
|
+
module: type === 'module',
|
|
128
|
+
});
|
|
129
|
+
el.remove();
|
|
130
|
+
}
|
|
131
|
+
if (!scripts.some((script) => script.src?.startsWith(baseUrl))) return null;
|
|
132
|
+
return { scripts, bodyHtml: parsed.body.innerHTML, inlineCss };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Does this document load `entryUrl` as a CLASSIC script?
|
|
137
|
+
*
|
|
138
|
+
* `false` for "as a module" AND for "does not mention it at all" — in both
|
|
139
|
+
* cases the ordinary module import is the right and unchanged behaviour.
|
|
140
|
+
*/
|
|
141
|
+
export function declaresClassicEntry(boot: DeclaredBoot | null, entryUrl: string): boolean {
|
|
142
|
+
const declared = boot?.scripts.find((script) => script.src === entryUrl);
|
|
143
|
+
return declared !== undefined && !declared.module;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Run the bundle exactly as its document declares: its body DOM first, then
|
|
148
|
+
* every script in order, as ONE classic script with `this === window`.
|
|
149
|
+
*
|
|
150
|
+
* The scripts are concatenated into a single function body rather than
|
|
151
|
+
* evaluated one at a time because that is what classic scripts on a page
|
|
152
|
+
* actually get — a shared variable environment. A webpack build communicates
|
|
153
|
+
* only through `this.webpackJsonp<name>`, so either shape boots it; a bundle
|
|
154
|
+
* that splits a `var` across two scripts needs the shared one.
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* Transplant the declared document's BODY into the game realm — the DOM the
|
|
158
|
+
* game's own scripts assume exists before they run (mount divs, and the
|
|
159
|
+
* non-JS `<script>` data tags `readDeclaredBoot` preserves). Shared by the
|
|
160
|
+
* classic boot below and by {@link import('./served-bundle').servedEntryLoader}'s
|
|
161
|
+
* MODULE branch: a module-entry game reads its page's elements exactly as a
|
|
162
|
+
* classic one does, and the module path used to import with an empty realm —
|
|
163
|
+
* `getElementById(...)` answered null and the mount failed by TypeError
|
|
164
|
+
* (three-points-waves, 2026-08-27). Idempotent per realm: a
|
|
165
|
+
* previous transplant is replaced, never doubled.
|
|
166
|
+
*/
|
|
167
|
+
export async function stageDeclaredDom(boot: DeclaredBoot): Promise<void> {
|
|
168
|
+
const realmDocument = ingestGameRealmWindow().document;
|
|
169
|
+
realmDocument.querySelector('[data-vgai-declared-dom]')?.remove();
|
|
170
|
+
if (boot.bodyHtml.trim().length === 0 && boot.inlineCss.length === 0) return;
|
|
171
|
+
const holder = realmDocument.createElement('div');
|
|
172
|
+
holder.setAttribute('data-vgai-declared-dom', '');
|
|
173
|
+
// `pointer-events:none` on the WRAPPER, restored per child: the holder is a
|
|
174
|
+
// host artifact, not part of the game's page, and it blanketed the game's
|
|
175
|
+
// canvas — a Babylon game whose whole UI is in-canvas (space-truckers' GUI
|
|
176
|
+
// 'press any key') never received a single click, which two human passes
|
|
177
|
+
// read as 'no input works' (2026-08-28). The game's own DOM children stay
|
|
178
|
+
// fully interactive; only the wrapper's empty area falls through.
|
|
179
|
+
holder.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none';
|
|
180
|
+
holder.innerHTML = boot.bodyHtml;
|
|
181
|
+
for (const child of Array.from(holder.children)) {
|
|
182
|
+
if (child instanceof HTMLElement && child.style.pointerEvents === '') {
|
|
183
|
+
child.style.pointerEvents = 'auto';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (boot.inlineCss.length > 0) {
|
|
187
|
+
// Scoped through the SAME transform declared sheets get — CSS applies
|
|
188
|
+
// document-wide wherever the element sits, so containment comes from
|
|
189
|
+
// `@scope`, and lifecycle comes from living inside the holder.
|
|
190
|
+
try {
|
|
191
|
+
const scopeCss = await loadGameCssScoper();
|
|
192
|
+
const style = realmDocument.createElement('style');
|
|
193
|
+
const scoped = boot.inlineCss.map(scopeCss).join('\n');
|
|
194
|
+
// Already scoped: the realm's injected-style observer must not scope it twice.
|
|
195
|
+
style.dataset[SCOPED_STYLE_MARK] = scoped;
|
|
196
|
+
style.textContent = scoped;
|
|
197
|
+
holder.prepend(style);
|
|
198
|
+
} catch {
|
|
199
|
+
// Unscopeable inline CSS stays uninstalled — never injected raw.
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
realmDocument.body.appendChild(holder);
|
|
203
|
+
// An id the page itself supplies wins over the host's stand-ins for it.
|
|
204
|
+
// Two stand-ins exist and both would shadow the real element in
|
|
205
|
+
// `getElementById` order: a host-made stub (appended before the page was
|
|
206
|
+
// staged) and the mount host itself, which carries `id="container"` for
|
|
207
|
+
// games that expect to mount into one (`ingest-root-adapter.ts`). The
|
|
208
|
+
// game's scripts have not run yet, so retiring them here loses nothing —
|
|
209
|
+
// and keeping them measured as css3d_periodictable's renderer landing on
|
|
210
|
+
// the HOST after the page's own menu, painting over its buttons.
|
|
211
|
+
const surface = realmDocument.body;
|
|
212
|
+
for (const staged of holder.querySelectorAll('[id]')) {
|
|
213
|
+
if (surface.id === staged.id) surface.removeAttribute('id');
|
|
214
|
+
for (const stub of surface.querySelectorAll(
|
|
215
|
+
`[data-vgai-dom-stub][id="${CSS.escape(staged.id)}"]`,
|
|
216
|
+
)) {
|
|
217
|
+
if (stub !== staged) stub.remove();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function bootDeclaredDocument(
|
|
223
|
+
boot: DeclaredBoot,
|
|
224
|
+
entryUrl: string,
|
|
225
|
+
fetchImpl: typeof fetch = globalThis.fetch,
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
await stageDeclaredDom(boot);
|
|
228
|
+
const sources: string[] = [];
|
|
229
|
+
for (const script of boot.scripts) {
|
|
230
|
+
// Module scripts belong to the module system, never to this classic
|
|
231
|
+
// concatenation — the entry itself is imported by `servedEntryLoader`,
|
|
232
|
+
// and evaluating module syntax as a classic script is a SyntaxError.
|
|
233
|
+
if (script.module) continue;
|
|
234
|
+
if (script.src === null) {
|
|
235
|
+
sources.push(script.inline);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
let response: Response;
|
|
239
|
+
try {
|
|
240
|
+
response = await fetchImpl(script.src);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// A bare TypeError('Failed to fetch') names nothing; the script URL is
|
|
243
|
+
// the whole diagnosis (a cross-origin CDN without CORS headers cannot
|
|
244
|
+
// be fetch()ed at all — script TAGS don't need CORS, fetch does).
|
|
245
|
+
throw new Error(
|
|
246
|
+
`served bundle boot: could not fetch its own declared <script src="${script.src}"> — ` +
|
|
247
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`served bundle boot: its own index.html declares <script src="${script.src}">, and that ` +
|
|
253
|
+
`URL answers ${response.status} ${response.statusText}.`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
sources.push(await response.text());
|
|
257
|
+
}
|
|
258
|
+
// `.call(window)` is the whole point: `window` here is the prelude's
|
|
259
|
+
// realm-shadowed binding, and a classic script's top-level `this` IS the
|
|
260
|
+
// window. An ES-module evaluation makes it `undefined`, which is the exact
|
|
261
|
+
// TypeError this path exists to stop.
|
|
262
|
+
// CLASSIC-PAGE GLOBAL SCOPING, scoped to what games actually author. On a
|
|
263
|
+
// real page, `window.THREE = ...` in one script and a bare `THREE` in the
|
|
264
|
+
// next are one binding. Here the write lands on the prelude's shadowed
|
|
265
|
+
// realm window, which a bare read never consults — measured twice
|
|
266
|
+
// (phaser-catch-the-cat's inline `new CatchTheCatGame(...)`,
|
|
267
|
+
// server-survival's vendor script reading `THREE`, 2026-08-27). A blanket
|
|
268
|
+
// `with (window)` is NOT the answer: it re-binds every bare identifier in
|
|
269
|
+
// minified code against ALL window properties and broke cuberun's webpack
|
|
270
|
+
// chunks outright. The scope object below answers `has` ONLY for keys that
|
|
271
|
+
// are not browser built-ins (pristine set, snapshotted before any script
|
|
272
|
+
// runs) — i.e. the globals the game itself authors — so built-ins and
|
|
273
|
+
// minified locals resolve exactly as before, while game-authored globals
|
|
274
|
+
// become one binding across scripts, page-fashion. Lookups consult the
|
|
275
|
+
// LIVE realm window each time, so script N sees what script N-1 wrote.
|
|
276
|
+
// The proxy TARGET is a plain empty object, never the window itself: `has`
|
|
277
|
+
// hiding a target's own non-configurable property is a proxy INVARIANT
|
|
278
|
+
// violation ("trap returned falsish for property 'window'"), so the traps
|
|
279
|
+
// delegate to the captured realm window instead of proxying it.
|
|
280
|
+
const scope =
|
|
281
|
+
'var __vgaiPristine = new Set(Object.getOwnPropertyNames(globalThis));\n' +
|
|
282
|
+
'__vgaiPristine.add("__vgaiPristine"); __vgaiPristine.add("__vgaiRealmGlobals");\n' +
|
|
283
|
+
'var __vgaiRealmGlobals = (function (w) {\n' +
|
|
284
|
+
' return new Proxy({}, {\n' +
|
|
285
|
+
' has: function (_, k) { return typeof k === "string" && !__vgaiPristine.has(k) && k in w; },\n' +
|
|
286
|
+
' get: function (_, k) { return typeof k === "string" ? w[k] : undefined; },\n' +
|
|
287
|
+
' set: function (_, k, v) { w[k] = v; return true; },\n' +
|
|
288
|
+
' });\n' +
|
|
289
|
+
'})(window);\n';
|
|
290
|
+
const body = `${GAME_GLOBALS_PRELUDE}\n${scope}\nreturn (function(){ with (__vgaiRealmGlobals) {\n${sources.join('\n;\n')}\n} }).call(window);`;
|
|
291
|
+
gameRealmPage().withCurrentScript(entryUrl, () => new Function(body)());
|
|
292
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `canvas`-surface MOUNT builder for in-tree ingest fixtures (was
|
|
3
|
+
* `ingest/discovery2d.ts`, whose name and glob both said "2d/pixi" about a
|
|
4
|
+
* surface that is a platform primitive: a canvas root may be Pixi, Phaser,
|
|
5
|
+
* Babylon, raw WebGL/WebGPU or a 2D context).
|
|
6
|
+
*
|
|
7
|
+
* Discovery moved to `registry.ts` (one glob, one directory, dispatched on the
|
|
8
|
+
* manifest's `surface`). What is left here is the genuinely per-surface part:
|
|
9
|
+
* turning a `canvas` entry into the `IngestGame2D` descriptor
|
|
10
|
+
* `ingest/mount-canvas-ingest-root.ts` mounts.
|
|
11
|
+
*
|
|
12
|
+
* ID CONTINUITY (§1.E/§1.G): each fixture's root id is UNCHANGED from its
|
|
13
|
+
* registry-era id (`bunnymark`/`flappy`/`tilemap-camera`, no `2d-` prefix).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
entryModules,
|
|
18
|
+
gameFileKey,
|
|
19
|
+
type IngestEntry,
|
|
20
|
+
ingestEntriesOnSurface,
|
|
21
|
+
} from '../host/ingest/registry';
|
|
22
|
+
import type { IngestGame2D } from '@volter/game-runtime/pixi/ingest';
|
|
23
|
+
import { composeIngestLoad } from './entry-load';
|
|
24
|
+
|
|
25
|
+
type BaseFields2D = Pick<IngestGame2D, 'id' | 'name' | 'description' | 'captureTimeoutMs'>;
|
|
26
|
+
|
|
27
|
+
function fixtureModuleLoader(folderId: string, relPath: string): () => Promise<unknown> {
|
|
28
|
+
const key = gameFileKey(folderId, relPath);
|
|
29
|
+
const load = entryModules[key];
|
|
30
|
+
if (!load) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`canvas ingest discovery: "${folderId}"'s module "${relPath}" did not match any game ` +
|
|
33
|
+
`module (looked for glob key "${key}").`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return load;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Build the mountable `IngestGame2D` for one `canvas`-surface registry entry. */
|
|
40
|
+
export function buildIngestGame2D({ folderId, manifest, root }: IngestEntry): IngestGame2D {
|
|
41
|
+
const { adapter } = root;
|
|
42
|
+
if (!root.entry) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`canvas ingest discovery: "${folderId}" has no \`entry\` — an ingest root mounts the ` +
|
|
45
|
+
"game's own entry module.",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const base: BaseFields2D = {
|
|
50
|
+
id: root.id,
|
|
51
|
+
name: manifest.name,
|
|
52
|
+
description: root.description ?? '',
|
|
53
|
+
...(adapter.captureTimeoutMs !== undefined
|
|
54
|
+
? { captureTimeoutMs: adapter.captureTimeoutMs }
|
|
55
|
+
: {}),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
...base,
|
|
60
|
+
load: composeIngestLoad(
|
|
61
|
+
fixtureModuleLoader(folderId, root.entry),
|
|
62
|
+
adapter.contractShim === undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: fixtureModuleLoader(folderId, adapter.contractShim),
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let _cache: IngestGame2D[] | null = null;
|
|
70
|
+
|
|
71
|
+
/** Every in-tree `canvas`-surface ingest fixture. */
|
|
72
|
+
export function discoverIngestGames2D(): IngestGame2D[] {
|
|
73
|
+
if (!_cache) {
|
|
74
|
+
_cache = ingestEntriesOnSurface('canvas')
|
|
75
|
+
.map(buildIngestGame2D)
|
|
76
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
77
|
+
}
|
|
78
|
+
return _cache;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Look up one discovered canvas ingest game by id. */
|
|
82
|
+
export function getIngestGame2D(id: string): IngestGame2D | undefined {
|
|
83
|
+
return discoverIngestGames2D().find((g) => g.id === id);
|
|
84
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dom`-surface MOUNT resolution for in-tree ingest fixtures (was
|
|
3
|
+
* `ingest/discovery-react.ts`, which named React in a seam that is about the
|
|
4
|
+
* DOM: a dom root may be React, Vue, Svelte or plain HTML).
|
|
5
|
+
*
|
|
6
|
+
* Discovery moved to `registry.ts` (one glob, one directory, dispatched on the
|
|
7
|
+
* manifest's `surface`). What is left here is the per-surface part, and for
|
|
8
|
+
* this surface it is small: a `dom` ingest root mounts through
|
|
9
|
+
* `resolveRootBinding` (`roots/react-root.ts`'s `resolveIngestReactAdapter`,
|
|
10
|
+
* N1) — the SAME `/@fs/`-dynamic-import + `createRoot` host stack `default-react`
|
|
11
|
+
* uses — rather than a capture-based bridge, so there is no descriptor with its
|
|
12
|
+
* own `load` thunk to build. The one thing the mount needs that discovery
|
|
13
|
+
* cannot give it is the fixture's ABSOLUTE on-disk folder, which is what
|
|
14
|
+
* {@link ingestGameDomProjectRoot} computes.
|
|
15
|
+
*
|
|
16
|
+
* `projectRoot` can't come from `import.meta.glob`/`import.meta.url` alone
|
|
17
|
+
* (those resolve to HTTP URLs in the browser, not filesystem paths) — it is
|
|
18
|
+
* built from `__VGAI_ENGINE_ROOT__` (a dev-server-only Vite `define`,
|
|
19
|
+
* `server/dev.ts`, mirroring the existing `__VGAI_PROJECT_PATH__` define) plus
|
|
20
|
+
* this folder's FIXED, known-at-build-time relative offset. `browser`
|
|
21
|
+
* editor-mode builds (no dev server) never define `__VGAI_ENGINE_ROOT__` —
|
|
22
|
+
* callers must check `getEditorMode() !== 'browser'` first (mirrors every other
|
|
23
|
+
* `/@fs/`-dependent ingest route's guard, e.g. `ingest/mount-ingest-root.ts`'s
|
|
24
|
+
* `tryManifestIngestRoute`).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { type IngestEntry, ingestEntriesOnSurface } from '../host/ingest/registry';
|
|
28
|
+
import type { ResolvedGameManifest } from '@volter/editor-project/manifest/load';
|
|
29
|
+
|
|
30
|
+
declare const __VGAI_ENGINE_ROOT__: string;
|
|
31
|
+
|
|
32
|
+
/** A discovered `dom`-surface ingest fixture: its parsed manifest (exactly one
|
|
33
|
+
* ingest root) plus that root's id for convenience. */
|
|
34
|
+
export interface IngestGameDom {
|
|
35
|
+
readonly folderId: string;
|
|
36
|
+
readonly manifest: ResolvedGameManifest;
|
|
37
|
+
readonly worldId: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Validate a `dom` entry against the surface's v1 scope and project it into
|
|
41
|
+
* the shape the mount route consumes. */
|
|
42
|
+
export function buildIngestGameDom({ folderId, manifest, root }: IngestEntry): IngestGameDom {
|
|
43
|
+
if (!root.entry) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`dom ingest discovery: "${folderId}" has no \`entry\` — a dom ingest root's \`entry\` ` +
|
|
46
|
+
'must point at the host-shim module in the same folder (D-N2).',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return { folderId, manifest, worldId: root.id };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let _cache: IngestGameDom[] | null = null;
|
|
53
|
+
|
|
54
|
+
/** Every in-tree `dom`-surface ingest fixture. */
|
|
55
|
+
export function discoverIngestGamesDom(): IngestGameDom[] {
|
|
56
|
+
if (!_cache) {
|
|
57
|
+
_cache = ingestEntriesOnSurface('dom')
|
|
58
|
+
.map(buildIngestGameDom)
|
|
59
|
+
.sort((a, b) => a.folderId.localeCompare(b.folderId));
|
|
60
|
+
}
|
|
61
|
+
return _cache;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Look up one discovered dom ingest fixture by its folder id (the same id
|
|
65
|
+
* resolution — the folder id IS the root id by convention, one root per
|
|
66
|
+
* manifest, but callers should key off `folderId` since that's what the URL
|
|
67
|
+
* param names). */
|
|
68
|
+
export function getIngestGameDom(folderId: string): IngestGameDom | undefined {
|
|
69
|
+
return discoverIngestGamesDom().find((g) => g.folderId === folderId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The absolute on-disk `projectRoot` for a discovered dom ingest fixture's
|
|
74
|
+
* folder — what `resolveRootBinding`/`resolveAllRoots` need to resolve its
|
|
75
|
+
* `entry`. `undefined` when `__VGAI_ENGINE_ROOT__` isn't defined (browser-mode
|
|
76
|
+
* static build — no dev server, no `/@fs/` route to resolve through anyway).
|
|
77
|
+
*/
|
|
78
|
+
export function ingestGameDomProjectRoot(folderId: string): string | undefined {
|
|
79
|
+
if (typeof __VGAI_ENGINE_ROOT__ !== 'string' || !__VGAI_ENGINE_ROOT__) return undefined;
|
|
80
|
+
const root = __VGAI_ENGINE_ROOT__.endsWith('/')
|
|
81
|
+
? __VGAI_ENGINE_ROOT__.slice(0, -1)
|
|
82
|
+
: __VGAI_ENGINE_ROOT__;
|
|
83
|
+
return `${root}/packages/editor/src/ingest/games/${folderId}`;
|
|
84
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `three`-surface MOUNT builder for in-tree ingest fixtures (was
|
|
3
|
+
* `ingest/discovery.ts`, which owned its own glob of `games/*`).
|
|
4
|
+
*
|
|
5
|
+
* Discovery is no longer here: `registry.ts` does the one glob over
|
|
6
|
+
* `./games/*\/vgai.project.json` and hands back parsed entries. This module's only
|
|
7
|
+
* job is turning a `three`-surface entry into the `IngestGame` descriptor
|
|
8
|
+
* `ingest/mount-ingest-root.ts` mounts — which is genuinely per-surface work, and is why the
|
|
9
|
+
* builders stayed split when discovery was unified.
|
|
10
|
+
*
|
|
11
|
+
* Module-loading choice (kept from the original): these are IN-TREE fixtures
|
|
12
|
+
* Vite already knows statically, so the descriptor is built from
|
|
13
|
+
* `import.meta.glob`s (`registry.ts` owns them) rather than through
|
|
14
|
+
* `resolveIngestDescriptor`'s `/@fs/` dynamic-import route, which exists for an
|
|
15
|
+
* EXTERNAL project folder:
|
|
16
|
+
* - entry modules (LAZY thunks) for the game's own entry (and its
|
|
17
|
+
* `contractShim`, when it declares one);
|
|
18
|
+
* - `?url` asset globs for the manifest's `assets` map.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
assetUrls,
|
|
23
|
+
entryModules,
|
|
24
|
+
gameFileKey,
|
|
25
|
+
type IngestEntry,
|
|
26
|
+
ingestEntriesOnSurface,
|
|
27
|
+
} from '../host/ingest/registry';
|
|
28
|
+
import { composeIngestLoad, declaredSourceModules, ingestDataWriter } from './entry-load';
|
|
29
|
+
import type { IngestGame } from './types';
|
|
30
|
+
|
|
31
|
+
function resolveAssetUrls(
|
|
32
|
+
folderId: string,
|
|
33
|
+
assets: Record<string, string>,
|
|
34
|
+
): Record<string, string> {
|
|
35
|
+
const resolved: Record<string, string> = {};
|
|
36
|
+
for (const [pathSubstring, relPath] of Object.entries(assets)) {
|
|
37
|
+
const key = gameFileKey(folderId, relPath);
|
|
38
|
+
const url = assetUrls[key];
|
|
39
|
+
if (!url) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`ingest discovery: "${folderId}"'s asset "${relPath}" did not match any served asset ` +
|
|
42
|
+
`(looked for glob key "${key}" — available: ${Object.keys(assetUrls).join(', ')})`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
resolved[pathSubstring] = url;
|
|
46
|
+
}
|
|
47
|
+
return resolved;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type BaseFields = Pick<
|
|
51
|
+
IngestGame,
|
|
52
|
+
| 'id'
|
|
53
|
+
| 'name'
|
|
54
|
+
| 'description'
|
|
55
|
+
| 'assets'
|
|
56
|
+
| 'domStubs'
|
|
57
|
+
| 'captureTimeoutMs'
|
|
58
|
+
| 'gameVersion'
|
|
59
|
+
| 'sourceModules'
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
/** The entry-module glob's thunk for one folder-relative module path. */
|
|
63
|
+
export function fixtureModuleLoader(folderId: string, relPath: string): () => Promise<unknown> {
|
|
64
|
+
const key = gameFileKey(folderId, relPath);
|
|
65
|
+
const load = entryModules[key];
|
|
66
|
+
if (!load) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`ingest discovery: "${folderId}"'s module "${relPath}" did not match any game ` +
|
|
69
|
+
`module (looked for glob key "${key}").`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return load;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Build the mountable `IngestGame` for one `three`-surface registry entry. */
|
|
76
|
+
export function buildIngestGame({ folderId, manifest, root }: IngestEntry): IngestGame {
|
|
77
|
+
const { adapter } = root;
|
|
78
|
+
if (!root.entry) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`ingest discovery: "${folderId}" has no \`entry\` — an ingest root mounts the game's own ` +
|
|
81
|
+
'entry module.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const base: BaseFields = {
|
|
86
|
+
id: root.id,
|
|
87
|
+
name: manifest.name,
|
|
88
|
+
description: root.description ?? '',
|
|
89
|
+
// The manifest's own `version`, so a later mount can compare a saved
|
|
90
|
+
// overlay's `authoredAgainst.gameVersion` against what's currently
|
|
91
|
+
// running.
|
|
92
|
+
gameVersion: manifest.version,
|
|
93
|
+
sourceModules: declaredSourceModules(root.entry, root.world?.entry),
|
|
94
|
+
...ingestDataWriter(adapter.dataWriter, (rel) => fixtureModuleLoader(folderId, rel)),
|
|
95
|
+
...(adapter.assets !== undefined ? { assets: resolveAssetUrls(folderId, adapter.assets) } : {}),
|
|
96
|
+
...(adapter.domStubs !== undefined ? { domStubs: adapter.domStubs } : {}),
|
|
97
|
+
...(adapter.captureTimeoutMs !== undefined
|
|
98
|
+
? { captureTimeoutMs: adapter.captureTimeoutMs }
|
|
99
|
+
: {}),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
load: composeIngestLoad(
|
|
105
|
+
fixtureModuleLoader(folderId, root.entry),
|
|
106
|
+
adapter.contractShim === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: fixtureModuleLoader(folderId, adapter.contractShim),
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let _cache: IngestGame[] | null = null;
|
|
114
|
+
|
|
115
|
+
/** Every in-tree `three`-surface ingest fixture. */
|
|
116
|
+
export function discoverIngestGames(): IngestGame[] {
|
|
117
|
+
if (!_cache) {
|
|
118
|
+
_cache = ingestEntriesOnSurface('three')
|
|
119
|
+
.map(buildIngestGame)
|
|
120
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
121
|
+
}
|
|
122
|
+
return _cache;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Look up one discovered three ingest game by its folder id. */
|
|
126
|
+
export function getIngestGame(id: string): IngestGame | undefined {
|
|
127
|
+
return discoverIngestGames().find((g) => g.id === id);
|
|
128
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `IngestGame` descriptor — the mountable shape `ingest/mount-three-ingest-root.ts`
|
|
3
|
+
* (`mountThreeIngestRoot`) and `authoring/ingest-root-adapter.ts`
|
|
4
|
+
* (`mountIngestGame`) consume, regardless of where it came from.
|
|
5
|
+
*
|
|
6
|
+
* It is the common contract every discovery frontend builds:
|
|
7
|
+
* `ingest/surface-three.ts`'s glob-based discovery of the in-tree fixture games
|
|
8
|
+
* under `ingest/games/<id>/vgai.project.json`, `binding-resolver.ts`'s
|
|
9
|
+
* `resolveIngestDescriptor` (an arbitrary project folder's manifest, mounted
|
|
10
|
+
* through the dev server), and `ingest/discovery-public-ingest.ts` (a game
|
|
11
|
+
* served out of the repo's public dir, by id).
|
|
12
|
+
*
|
|
13
|
+
* Every field is a mounting FACT. The host runs the game's own entry module in
|
|
14
|
+
* the editor's realm — there is no choice of route to declare.
|
|
15
|
+
*/
|
|
16
|
+
export interface IngestGame {
|
|
17
|
+
/** Stable id (used by the launcher + conformance test; == the manifest world id). */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Human label shown in the editor's game menu (== the manifest's game name). */
|
|
20
|
+
name: string;
|
|
21
|
+
/** One-line description (== the manifest world's optional `description`, or `''`). */
|
|
22
|
+
description: string;
|
|
23
|
+
/**
|
|
24
|
+
* Import + run the unmodified game module. When the manifest declares a
|
|
25
|
+
* `contractShim`, this thunk imports that module first — same realm, same
|
|
26
|
+
* tick order the shim's timing contract describes.
|
|
27
|
+
*/
|
|
28
|
+
load: () => Promise<unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* The game's own DATA writer, when it declares one (`ingest.dataWriter`) —
|
|
31
|
+
* the module that turns an authored move of an object the game placed from a
|
|
32
|
+
* level file into new bytes of that file. Carried as its specifier plus a
|
|
33
|
+
* loader, because the loader is the only thing that knows how this game's
|
|
34
|
+
* files are served (globbed fixture, `public/` url, `/@fs/` folder) and the
|
|
35
|
+
* specifier is what a refusal has to name. Absent ⇒ this game's authorable
|
|
36
|
+
* truth is source only, and a record-anchored object refuses by name.
|
|
37
|
+
*/
|
|
38
|
+
dataWriter?: { specifier: string; load: () => Promise<unknown> };
|
|
39
|
+
/**
|
|
40
|
+
* The PROJECT-RELATIVE paths of the game's own source modules this root
|
|
41
|
+
* declares — its `entry` and, when it names one, `world.entry`.
|
|
42
|
+
*
|
|
43
|
+
* The lane decision reads them (`authoring/ingest-root-adapter.ts`,
|
|
44
|
+
* `ingestSourceRoots`): their parent directories are the scope in which a
|
|
45
|
+
* serve-time OID stamp counts as THIS root's. Carried as declared strings
|
|
46
|
+
* rather than resolved paths because a discovery frontend does not always
|
|
47
|
+
* know the project's absolute location; the mount does (`getCurrentProject`).
|
|
48
|
+
* Absent ⇒ only the project folder itself scopes the serve-time door.
|
|
49
|
+
*/
|
|
50
|
+
sourceModules?: readonly string[];
|
|
51
|
+
/** How long to wait for the game's first captured frame before failing. */
|
|
52
|
+
captureTimeoutMs?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Map of game-relative-path substring → served asset URL. The ingest adapter
|
|
55
|
+
* installs a `THREE.DefaultLoadingManager` URL modifier that rewrites any
|
|
56
|
+
* requested URL containing a key to its value.
|
|
57
|
+
*/
|
|
58
|
+
assets?: Record<string, string>;
|
|
59
|
+
/**
|
|
60
|
+
* Element ids the game expects to already exist in the DOM (e.g. an FPS game's
|
|
61
|
+
* `#blocker`/`#instructions` overlay). The host creates hidden stub elements
|
|
62
|
+
* with these ids before running the game, so DOM-dependent games stay
|
|
63
|
+
* unmodified.
|
|
64
|
+
*/
|
|
65
|
+
domStubs?: string[];
|
|
66
|
+
/**
|
|
67
|
+
* The game's own manifest `version` (D9 — orphan/mismatch detection), when
|
|
68
|
+
* this descriptor was built straight from a manifest that has one.
|
|
69
|
+
* `ingest/surface-three.ts`'s in-tree fixture route and
|
|
70
|
+
* `ingest/discovery-public-ingest.ts` set this from the parsed manifest's
|
|
71
|
+
* `version`; `ingest/resolve-three.ts`'s `resolveIngestDescriptor` (the
|
|
72
|
+
* CLI-on-folder / manifest-world route) cannot — it only has a
|
|
73
|
+
* `ResolvedAdapterRoot`, not the manifest's top-level `version`.
|
|
74
|
+
*/
|
|
75
|
+
gameVersion?: string;
|
|
76
|
+
}
|