@vgai/engine 0.5.21 → 0.5.23
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/README.md +6 -3
- package/dist/adapter/adapter-module.d.ts.map +1 -1
- package/dist/adapter/adapter-module.js +2 -4
- package/dist/adapter/adapter-surface.d.ts +1 -1
- package/dist/adapter/adapter-surface.js +1 -1
- package/dist/adapter/authoring.d.ts +19 -21
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/authoring.js +2 -2
- package/dist/adapter/binding.d.ts +341 -0
- package/dist/adapter/binding.d.ts.map +1 -0
- package/dist/adapter/binding.js +148 -0
- package/dist/adapter/colyseus-networking-adapter.d.ts.map +1 -1
- package/dist/adapter/colyseus-networking-adapter.js +23 -4
- package/dist/adapter/entrypoint-selection-readers.d.ts +24 -0
- package/dist/adapter/entrypoint-selection-readers.d.ts.map +1 -0
- package/dist/adapter/entrypoint-selection-readers.js +63 -0
- package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -1
- package/dist/adapter/entrypoint-selection-source.js +1 -44
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +1 -52
- package/dist/adapter/host-context.d.ts +46 -15
- package/dist/adapter/host-context.d.ts.map +1 -1
- package/dist/adapter/host-context.js +2 -9
- package/dist/adapter/index.d.ts +11 -12
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +9 -10
- package/dist/adapter/ingest/game-contract.d.ts +0 -3
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +0 -3
- package/dist/adapter/ingest/scene-capture.d.ts +19 -23
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +143 -17
- package/dist/adapter/ingest/visible-capture-window.d.ts +34 -0
- package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
- package/dist/adapter/manifest-interpreter.d.ts +59 -0
- package/dist/adapter/manifest-interpreter.d.ts.map +1 -0
- package/dist/adapter/manifest-interpreter.js +50 -0
- package/dist/adapter/native-debug-module.d.ts +71 -3
- package/dist/adapter/native-debug-module.d.ts.map +1 -1
- package/dist/adapter/native-debug-module.js +146 -10
- package/dist/adapter/rapier-physics-adapter.d.ts +31 -1
- package/dist/adapter/rapier-physics-adapter.d.ts.map +1 -1
- package/dist/adapter/rapier-physics-adapter.js +50 -36
- package/dist/adapter/renderer-config.d.ts +3 -3
- package/dist/adapter/renderer-config.js +3 -3
- package/dist/adapter/root-adapter.d.ts +7 -7
- package/dist/adapter/root-adapter.js +4 -4
- package/dist/adapter/system-adapter.d.ts +45 -7
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/adapter/system-seam-contract.d.ts +3 -3
- package/dist/adapter/system-seam-contract.js +3 -3
- package/dist/adapter/system-slot.d.ts +168 -0
- package/dist/adapter/system-slot.d.ts.map +1 -0
- package/dist/adapter/system-slot.js +229 -0
- package/dist/adapter/transform.d.ts +11 -1
- package/dist/adapter/transform.d.ts.map +1 -1
- package/dist/ai/navigation.d.ts +17 -1
- package/dist/ai/navigation.d.ts.map +1 -1
- package/dist/ai/navigation.js +26 -5
- package/dist/animation/anim-graph-types.d.ts +11 -3
- package/dist/animation/anim-graph-types.d.ts.map +1 -1
- package/dist/animation/theatre-clock-binding.d.ts +4 -4
- package/dist/animation/theatre-clock-binding.js +4 -4
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
- package/dist/animation/xstate-animation-binding.js +2 -2
- package/dist/animation/xstate-animation-meta.d.ts +13 -0
- package/dist/animation/xstate-animation-meta.d.ts.map +1 -1
- package/dist/animation/xstate-animation-meta.js +14 -0
- package/dist/asset-formats/index.d.ts +4 -7
- package/dist/asset-formats/index.d.ts.map +1 -1
- package/dist/asset-formats/index.js +4 -7
- package/dist/asset-formats/render-env.d.ts +5 -8
- package/dist/asset-formats/render-env.d.ts.map +1 -1
- package/dist/asset-formats/render-env.js +3 -6
- package/dist/asset-parse-error.d.ts +2 -5
- package/dist/asset-parse-error.d.ts.map +1 -1
- package/dist/asset-parse-error.js +2 -5
- package/dist/audio/bus-mixer.d.ts +101 -0
- package/dist/audio/bus-mixer.d.ts.map +1 -0
- package/dist/audio/bus-mixer.js +115 -0
- package/dist/canvas-react/index.d.ts +5 -12
- package/dist/canvas-react/index.d.ts.map +1 -1
- package/dist/canvas-react/index.js +5 -12
- package/dist/canvas-react/pixi-react-root-factory.d.ts +34 -21
- package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-root-factory.js +274 -36
- package/dist/core/countdown-timer.d.ts +118 -0
- package/dist/core/countdown-timer.d.ts.map +1 -0
- package/dist/core/countdown-timer.js +137 -0
- package/dist/core/deferred-commands.d.ts +127 -0
- package/dist/core/deferred-commands.d.ts.map +1 -0
- package/dist/core/deferred-commands.js +132 -0
- package/dist/core/frame-pacing.d.ts +1 -1
- package/dist/core/frame-pacing.js +2 -2
- package/dist/core/game-loop.d.ts +1 -3
- package/dist/core/game-loop.d.ts.map +1 -1
- package/dist/core/game-loop.js +1 -3
- package/dist/core/seeded-random.d.ts +1 -1
- package/dist/core/sim-clock.d.ts +19 -8
- package/dist/core/sim-clock.d.ts.map +1 -1
- package/dist/core/sim-clock.js +18 -7
- package/dist/core/types.d.ts +1 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/defaults.d.ts +6 -14
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +6 -14
- package/dist/dev/instruments.d.ts +38 -11
- package/dist/dev/instruments.d.ts.map +1 -1
- package/dist/dev/instruments.js +81 -11
- package/dist/dev/register-render-vitals.d.ts +1 -4
- package/dist/dev/register-render-vitals.d.ts.map +1 -1
- package/dist/dev/register-render-vitals.js +1 -4
- package/dist/dev/render-debug-adapter.d.ts +2 -3
- package/dist/dev/render-debug-adapter.d.ts.map +1 -1
- package/dist/dev/render-debug-adapter.js +2 -3
- package/dist/dev/render-vitals.d.ts +3 -3
- package/dist/dev/render-vitals.js +3 -3
- package/dist/dev/webgl-frame-capture.d.ts +1 -1
- package/dist/dev/webgl-frame-capture.js +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/input/host-pointer.d.ts +122 -0
- package/dist/input/host-pointer.d.ts.map +1 -0
- package/dist/input/host-pointer.js +160 -0
- package/dist/input/input-manager.d.ts +33 -5
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +48 -13
- package/dist/input/rebind-controller.d.ts.map +1 -1
- package/dist/input/rebind-controller.js +6 -8
- package/dist/input/schema.d.ts.map +1 -1
- package/dist/input/schema.js +56 -28
- package/dist/manifest/filename.d.ts.map +1 -1
- package/dist/manifest/filename.js +8 -12
- package/dist/manifest/load.d.ts +10 -10
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +9 -6
- package/dist/manifest/schema.d.ts +17 -21
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +45 -35
- package/dist/physics/collider-dimensions.d.ts +8 -12
- package/dist/physics/collider-dimensions.d.ts.map +1 -1
- package/dist/physics/collider-dimensions.js +8 -12
- package/dist/pixi/scene-capture.d.ts +11 -13
- package/dist/pixi/scene-capture.d.ts.map +1 -1
- package/dist/react/world-state.d.ts +1 -1
- package/dist/react/world-state.d.ts.map +1 -1
- package/dist/render/directional-shadow-fit.d.ts +88 -0
- package/dist/render/directional-shadow-fit.d.ts.map +1 -0
- package/dist/render/directional-shadow-fit.js +133 -0
- package/dist/render/environment-capture.d.ts +68 -0
- package/dist/render/environment-capture.d.ts.map +1 -0
- package/dist/render/environment-capture.js +90 -0
- package/dist/render/ibl-override-material.d.ts +97 -0
- package/dist/render/ibl-override-material.d.ts.map +1 -0
- package/dist/render/ibl-override-material.js +114 -0
- package/dist/render/light-camera-factory.d.ts +5 -7
- package/dist/render/light-camera-factory.d.ts.map +1 -1
- package/dist/render/light-camera-factory.js +8 -11
- package/dist/render/material-factory.js +1 -1
- package/dist/render/render-batch-system.d.ts.map +1 -1
- package/dist/render/render-features.d.ts +17 -6
- package/dist/render/render-features.d.ts.map +1 -1
- package/dist/render/render-features.js +0 -28
- package/dist/runtime/create-runtime.d.ts +34 -25
- package/dist/runtime/create-runtime.d.ts.map +1 -1
- package/dist/runtime/create-runtime.js +218 -93
- package/dist/runtime/debug-registry.d.ts +77 -8
- package/dist/runtime/debug-registry.d.ts.map +1 -1
- package/dist/runtime/debug-registry.js +3 -3
- package/dist/runtime/dev-build.d.ts +2 -2
- package/dist/runtime/dev-build.js +2 -2
- package/dist/runtime/game-input-seams.d.ts +51 -0
- package/dist/runtime/game-input-seams.d.ts.map +1 -0
- package/dist/runtime/game-input-seams.js +86 -0
- package/dist/runtime/game.d.ts +104 -40
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +119 -61
- package/dist/runtime/mount-game.d.ts +0 -16
- package/dist/runtime/mount-game.d.ts.map +1 -1
- package/dist/runtime/mount-game.js +26 -41
- package/dist/runtime/mount-manifest.d.ts +22 -14
- package/dist/runtime/mount-manifest.d.ts.map +1 -1
- package/dist/runtime/mount-manifest.js +22 -20
- package/dist/runtime/playtest.d.ts +22 -0
- package/dist/runtime/playtest.d.ts.map +1 -0
- package/dist/runtime/playtest.js +12 -0
- package/dist/runtime/state-bridge.d.ts +2 -3
- package/dist/runtime/state-bridge.d.ts.map +1 -1
- package/dist/runtime/state-bridge.js +1 -0
- package/dist/setup/setup-audio.d.ts +12 -0
- package/dist/setup/setup-audio.d.ts.map +1 -1
- package/dist/setup/setup-audio.js +9 -10
- package/dist/setup/setup-renderer.d.ts +2 -25
- package/dist/setup/setup-renderer.d.ts.map +1 -1
- package/dist/setup/setup-renderer.js +2 -31
- package/dist/world3d-react/index.d.ts +15 -25
- package/dist/world3d-react/index.d.ts.map +1 -1
- package/dist/world3d-react/index.js +15 -25
- package/dist/world3d-react/r3f-root-factory.d.ts +28 -34
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +403 -57
- package/dist/world3d-react/rapier-physics-bridge.d.ts +30 -17
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +143 -58
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +16 -14
- package/schemas/vgai-project.schema.json +35 -26
- package/src/adapter/adapter-module.ts +2 -4
- package/src/adapter/adapter-surface.ts +1 -1
- package/src/adapter/authoring.ts +19 -21
- package/src/adapter/binding.ts +468 -0
- package/src/adapter/colyseus-networking-adapter.ts +20 -5
- package/src/adapter/entrypoint-selection-readers.ts +66 -0
- package/src/adapter/entrypoint-selection-source.ts +1 -40
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +1 -48
- package/src/adapter/host-context.ts +45 -15
- package/src/adapter/index.ts +23 -11
- package/src/adapter/ingest/game-contract.ts +0 -3
- package/src/adapter/ingest/scene-capture.ts +190 -39
- package/src/adapter/ingest/visible-capture-window.ts +35 -0
- package/src/adapter/manifest-interpreter.ts +64 -0
- package/src/adapter/native-debug-module.ts +248 -15
- package/src/adapter/rapier-physics-adapter.ts +76 -20
- package/src/adapter/renderer-config.ts +3 -3
- package/src/adapter/root-adapter.ts +7 -7
- package/src/adapter/system-adapter.ts +43 -8
- package/src/adapter/system-seam-contract.ts +3 -3
- package/src/adapter/system-slot.ts +291 -0
- package/src/adapter/transform.ts +18 -1
- package/src/ai/navigation.ts +30 -3
- package/src/animation/anim-graph-types.ts +11 -3
- package/src/animation/theatre-clock-binding.ts +4 -4
- package/src/animation/xstate-animation-binding.ts +2 -2
- package/src/animation/xstate-animation-meta.ts +16 -0
- package/src/asset-formats/index.ts +4 -7
- package/src/asset-formats/render-env.ts +3 -6
- package/src/asset-parse-error.ts +2 -5
- package/src/audio/bus-mixer.ts +161 -0
- package/src/canvas-react/index.ts +5 -21
- package/src/canvas-react/pixi-react-root-factory.tsx +301 -39
- package/src/core/countdown-timer.ts +188 -0
- package/src/core/deferred-commands.ts +174 -0
- package/src/core/frame-pacing.ts +2 -2
- package/src/core/game-loop.ts +1 -3
- package/src/core/seeded-random.ts +1 -1
- package/src/core/sim-clock.ts +19 -8
- package/src/core/types.ts +1 -3
- package/src/defaults.ts +6 -14
- package/src/dev/instruments.ts +108 -11
- package/src/dev/register-render-vitals.ts +1 -4
- package/src/dev/render-debug-adapter.ts +2 -3
- package/src/dev/render-vitals.ts +3 -3
- package/src/dev/webgl-frame-capture.ts +1 -1
- package/src/index.ts +2 -5
- package/src/input/host-pointer.ts +230 -0
- package/src/input/input-manager.ts +49 -13
- package/src/input/input-types.ts +1 -1
- package/src/input/rebind-controller.ts +6 -8
- package/src/input/schema.ts +216 -188
- package/src/manifest/filename.ts +8 -12
- package/src/manifest/load.ts +17 -13
- package/src/manifest/schema.ts +55 -47
- package/src/physics/collider-dimensions.ts +8 -12
- package/src/pixi/scene-capture.ts +12 -15
- package/src/react/world-state.tsx +1 -1
- package/src/render/directional-shadow-fit.ts +156 -0
- package/src/render/environment-capture.ts +102 -0
- package/src/render/ibl-override-material.ts +170 -0
- package/src/render/light-camera-factory.ts +8 -11
- package/src/render/material-factory.ts +1 -1
- package/src/render/render-batch-system.ts +14 -6
- package/src/render/render-features.ts +17 -35
- package/src/runtime/create-runtime.ts +270 -108
- package/src/runtime/debug-registry.ts +84 -8
- package/src/runtime/dev-build.ts +2 -2
- package/src/runtime/game-input-seams.ts +108 -0
- package/src/runtime/game.ts +231 -85
- package/src/runtime/mount-game.ts +31 -57
- package/src/runtime/mount-manifest.ts +47 -36
- package/src/runtime/playtest.ts +22 -0
- package/src/runtime/state-bridge.ts +3 -3
- package/src/setup/setup-audio.ts +21 -11
- package/src/setup/setup-renderer.ts +2 -61
- package/src/world3d-react/index.ts +15 -31
- package/src/world3d-react/r3f-root-factory.tsx +451 -60
- package/src/world3d-react/rapier-physics-bridge.tsx +154 -58
- package/dist/adapter/setup-three-root-adapter.d.ts +0 -86
- package/dist/adapter/setup-three-root-adapter.d.ts.map +0 -1
- package/dist/adapter/setup-three-root-adapter.js +0 -908
- package/dist/animation/clip-map.d.ts +0 -12
- package/dist/animation/clip-map.d.ts.map +0 -1
- package/dist/animation/clip-map.js +0 -31
- package/dist/asset-registry.d.ts +0 -38
- package/dist/asset-registry.d.ts.map +0 -1
- package/dist/asset-registry.js +0 -66
- package/dist/canvas-react/engine-bridge.d.ts +0 -45
- package/dist/canvas-react/engine-bridge.d.ts.map +0 -1
- package/dist/canvas-react/engine-bridge.js +0 -45
- package/dist/canvas-react/pixi-react-adapter.d.ts +0 -77
- package/dist/canvas-react/pixi-react-adapter.d.ts.map +0 -1
- package/dist/canvas-react/pixi-react-adapter.js +0 -294
- package/dist/canvas-react/world-context.d.ts +0 -98
- package/dist/canvas-react/world-context.d.ts.map +0 -1
- package/dist/canvas-react/world-context.js +0 -173
- package/dist/dev/debug-draw.d.ts +0 -24
- package/dist/dev/debug-draw.d.ts.map +0 -1
- package/dist/dev/debug-draw.js +0 -73
- package/dist/render/auto-batcher.d.ts +0 -34
- package/dist/render/auto-batcher.d.ts.map +0 -1
- package/dist/render/auto-batcher.js +0 -140
- package/dist/render/lod.d.ts +0 -13
- package/dist/render/lod.d.ts.map +0 -1
- package/dist/render/lod.js +0 -16
- package/dist/runtime/types.d.ts +0 -303
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/runtime/types.js +0 -1
- package/dist/world3d-react/engine-bridge.d.ts +0 -47
- package/dist/world3d-react/engine-bridge.d.ts.map +0 -1
- package/dist/world3d-react/engine-bridge.js +0 -73
- package/dist/world3d-react/r3f-adapter.d.ts +0 -58
- package/dist/world3d-react/r3f-adapter.d.ts.map +0 -1
- package/dist/world3d-react/r3f-adapter.js +0 -634
- package/dist/world3d-react/world-context.d.ts +0 -182
- package/dist/world3d-react/world-context.d.ts.map +0 -1
- package/dist/world3d-react/world-context.js +0 -235
- package/src/adapter/setup-three-root-adapter.ts +0 -1032
- package/src/animation/clip-map.ts +0 -34
- package/src/asset-registry.ts +0 -89
- package/src/canvas-react/engine-bridge.ts +0 -59
- package/src/canvas-react/pixi-react-adapter.tsx +0 -356
- package/src/canvas-react/world-context.ts +0 -253
- package/src/dev/debug-draw.ts +0 -80
- package/src/render/auto-batcher.ts +0 -168
- package/src/render/lod.ts +0 -17
- package/src/runtime/types.ts +0 -328
- package/src/world3d-react/engine-bridge.ts +0 -86
- package/src/world3d-react/r3f-adapter.tsx +0 -717
- package/src/world3d-react/world-context.ts +0 -358
package/src/runtime/game.ts
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
import type * as PIXI from 'pixi.js';
|
|
13
13
|
import type * as THREE from 'three';
|
|
14
14
|
import type { AdapterSurface as AdapterSurfaceLeaf } from '../adapter/adapter-surface';
|
|
15
|
+
import type { RootBinding } from '../adapter/binding';
|
|
15
16
|
import { formatAudioGateMessage, formatLoopGateMessage } from '../adapter/loop-gate-report';
|
|
16
17
|
import type { MountedRoot } from '../adapter/root-adapter';
|
|
17
|
-
import type { MountedSetupThreeRoot } from '../adapter/setup-three-root-adapter';
|
|
18
18
|
import type { SystemAdapters } from '../adapter/system-adapter';
|
|
19
19
|
import type { AssetCache } from '../assets';
|
|
20
20
|
import type { createGameLoop } from '../core/game-loop';
|
|
@@ -41,15 +41,15 @@ import {
|
|
|
41
41
|
registerDebugRegistry,
|
|
42
42
|
} from './debug-registry';
|
|
43
43
|
import { createGameplayRngTrap, registerGameplayRngTrapControl } from './gameplay-rng-trap';
|
|
44
|
+
import type { PlaytestContext } from './playtest';
|
|
44
45
|
import { createStateBridge, type GameStateBridge } from './state-bridge';
|
|
45
|
-
import type { GameContext, PlaytestContext } from './types';
|
|
46
46
|
|
|
47
47
|
/** The one loop type — `createGameLoop`'s return shape (fixed-step sim,
|
|
48
|
-
* display-rate presentation
|
|
48
|
+
* display-rate presentation). */
|
|
49
49
|
export type GameLoop = ReturnType<typeof createGameLoop>;
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
52
|
+
* The tail of `PHASE_ORDER` that presents rather than simulates, and
|
|
53
53
|
* therefore runs once per DISPLAY frame (`runRenderFrameImpl`) rather than
|
|
54
54
|
* once per fixed substep. The same two phases `runTicks`' `skipRenderPhases`
|
|
55
55
|
* fast-forward skips and `render-control.ts`'s `renderOnce()` drives — one
|
|
@@ -68,10 +68,10 @@ const DISPLAY_RATE_PHASES: readonly SystemPhaseName[] = PHASE_ORDER.filter(
|
|
|
68
68
|
* freezes. A paused-and-pausable world's `render` phase still runs (with `dt`
|
|
69
69
|
* forced to `0`, so time-based render effects — e.g. a post-processing pass with
|
|
70
70
|
* its own internal clock — don't silently keep animating under a "frozen" scene)
|
|
71
|
-
* — simulation freezes, the screen does not go black.
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* `runFrame`. `GameLoop.timeScale`
|
|
71
|
+
* — simulation freezes, the screen does not go black. That render runs once per
|
|
72
|
+
* display frame under a host that drives `runRenderFrame`, and once per substep
|
|
73
|
+
* under one that still renders inside
|
|
74
|
+
* `runFrame`. `GameLoop.timeScale` remains the orthogonal
|
|
75
75
|
* "speed up/slow down" axis — pausing never touches it, so the host's
|
|
76
76
|
* accumulator/rAF loop keeps ticking at its normal cadence, which is what makes
|
|
77
77
|
* "paused still renders" possible.
|
|
@@ -119,7 +119,7 @@ export interface PlayState {
|
|
|
119
119
|
* `adapter/root-adapter.ts`'s kind-tagged `MountedRoot` types can name it
|
|
120
120
|
* without an import cycle back to this file. This re-export keeps every
|
|
121
121
|
* existing `import type { AdapterSurface } from '../runtime/game'` call site
|
|
122
|
-
*
|
|
122
|
+
* compiling unchanged.
|
|
123
123
|
*/
|
|
124
124
|
export type AdapterSurface = AdapterSurfaceLeaf;
|
|
125
125
|
|
|
@@ -145,6 +145,24 @@ export interface RootFrameHooks {
|
|
|
145
145
|
/**
|
|
146
146
|
* A single world instance: the unit of adaptation.
|
|
147
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* One `SystemAdapters` slot a mounted root's `systems` table answered with
|
|
150
|
+
* `absent(reason)` (`adapter/system-slot.ts`) — a POSITIVE absence.
|
|
151
|
+
*
|
|
152
|
+
* Nothing is installed for it. It exists so a reader can tell "this game has no
|
|
153
|
+
* physics" from "nobody looked", which the adapter bag alone cannot do: an
|
|
154
|
+
* unfilled slot has the same shape for both, and only one of them is a work
|
|
155
|
+
* order.
|
|
156
|
+
*/
|
|
157
|
+
export interface DeclaredSystemAbsence {
|
|
158
|
+
/** The root whose declaration answered — absences are declared per root, and
|
|
159
|
+
* the slot is only truly absent when NO root fills it. */
|
|
160
|
+
readonly rootId: string;
|
|
161
|
+
readonly slot: keyof SystemAdapters;
|
|
162
|
+
/** The game's own words: what was searched, and what was found. */
|
|
163
|
+
readonly reason: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
148
166
|
export interface RootInstance {
|
|
149
167
|
/** Manifest root id. */
|
|
150
168
|
readonly id: string;
|
|
@@ -198,6 +216,39 @@ export interface RootInstance {
|
|
|
198
216
|
* instead (unless it `drivesOwnLoop`, in which case it isn't ticked at
|
|
199
217
|
* all — see `runFrame`). */
|
|
200
218
|
readonly frame?: RootFrameHooks | undefined;
|
|
219
|
+
/** True from the moment this world's `mounted.dispose()` runs. There is no
|
|
220
|
+
* `unregisterRoot`, so a disposed world stays in `Game.roots`; `runFrame`
|
|
221
|
+
* reads this to skip it entirely — frame hooks AND the opaque
|
|
222
|
+
* `mounted.update` fallback (see the disposed-world guard in
|
|
223
|
+
* `createRootInstance`). */
|
|
224
|
+
readonly disposed: boolean;
|
|
225
|
+
/**
|
|
226
|
+
* THIS ROOT'S WHOLE DECLARATION, BOUND — `adapter/binding.ts`'s
|
|
227
|
+
* {@link RootBinding}, assembled at registration from this instance's own
|
|
228
|
+
* `adapter`/`mounted`/`pausable` plus the {@link RootDeclaration} the
|
|
229
|
+
* resolver produced. Every member of it is a reference to something already
|
|
230
|
+
* reachable through the fields above; it is the ONE place the five protocol
|
|
231
|
+
* families are addressed by name rather than re-assembled per consumer.
|
|
232
|
+
*
|
|
233
|
+
* `null` in exactly ONE honest case, and never as a degrade: the
|
|
234
|
+
* registration carried NO declaration — a bare
|
|
235
|
+
* `registerThreeRoot(game, adapter, mounted)` from a test harness, or a
|
|
236
|
+
* `mountManifestRoots` caller that supplied an already-constructed adapter
|
|
237
|
+
* with no entry namespace behind it. There is no manifest root, no parsed
|
|
238
|
+
* definition and no entry surface to bind, and fabricating them would be
|
|
239
|
+
* the anti-shim rule broken at the seam that exists to hold it. Every root
|
|
240
|
+
* the editor resolves through `resolveAllRoots`/`resolveAllRootEntries`
|
|
241
|
+
* carries a declaration, so its binding is non-null.
|
|
242
|
+
*
|
|
243
|
+
* A mount that exposes no `authoring` does NOT null this out; it costs that
|
|
244
|
+
* root its `projection` and `truth` members only (see
|
|
245
|
+
* {@link RootBinding.projection}). That distinction is load-bearing: the
|
|
246
|
+
* editor supplies native TSX roots' authoring itself, so treating an
|
|
247
|
+
* absent `mounted.authoring` as an absent BINDING made `binding` null for
|
|
248
|
+
* every first-party root and left four perfectly constructible families
|
|
249
|
+
* unreadable.
|
|
250
|
+
*/
|
|
251
|
+
readonly binding: RootBinding | null;
|
|
201
252
|
}
|
|
202
253
|
|
|
203
254
|
/**
|
|
@@ -236,6 +287,9 @@ export interface RootInstanceInit {
|
|
|
236
287
|
readonly physics2d?: Physics2DRegistry | undefined;
|
|
237
288
|
readonly camera?: unknown;
|
|
238
289
|
readonly frame?: RootFrameHooks | undefined;
|
|
290
|
+
/** This root's bound declaration — see {@link RootInstance.binding} for
|
|
291
|
+
* when it is legitimately absent. */
|
|
292
|
+
readonly binding?: RootBinding | null | undefined;
|
|
239
293
|
}
|
|
240
294
|
|
|
241
295
|
/**
|
|
@@ -259,6 +313,7 @@ export function createRootInstance(init: RootInstanceInit): RootInstance {
|
|
|
259
313
|
physics2d,
|
|
260
314
|
camera,
|
|
261
315
|
frame,
|
|
316
|
+
binding = null,
|
|
262
317
|
} = init;
|
|
263
318
|
|
|
264
319
|
// A three world with no scene has nothing for `threeScene()` to return —
|
|
@@ -302,11 +357,11 @@ export function createRootInstance(init: RootInstanceInit): RootInstance {
|
|
|
302
357
|
// read, so this wraps `mounted.dispose` in place (mutating the SAME mount
|
|
303
358
|
// object every holder of `mounted` shares — calling `mounted.dispose()`
|
|
304
359
|
// directly, exactly like calling `world.mounted.dispose()`, trips this)
|
|
305
|
-
// to flip a
|
|
306
|
-
// once
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
360
|
+
// to flip a flag this instance exposes as `disposed`, and wraps the frame
|
|
361
|
+
// hooks so they silently no-op once it is set. `runFrame` reads `disposed`
|
|
362
|
+
// and skips the world outright, which is what also covers the OPAQUE
|
|
363
|
+
// `mounted.update` fallback — the path every three root takes now that a
|
|
364
|
+
// mount is a plain `RootAdapter` with no phase-partitioned entry point.
|
|
310
365
|
let disposed = false;
|
|
311
366
|
const originalDispose = mounted.dispose.bind(mounted);
|
|
312
367
|
mounted.dispose = () => {
|
|
@@ -337,6 +392,10 @@ export function createRootInstance(init: RootInstanceInit): RootInstance {
|
|
|
337
392
|
physics2d,
|
|
338
393
|
camera,
|
|
339
394
|
frame: guardedFrame,
|
|
395
|
+
binding,
|
|
396
|
+
get disposed(): boolean {
|
|
397
|
+
return disposed;
|
|
398
|
+
},
|
|
340
399
|
threeScene(): THREE.Scene {
|
|
341
400
|
if (kind !== 'three' || !scene) {
|
|
342
401
|
throw new Error(
|
|
@@ -374,26 +433,9 @@ export function createRootInstance(init: RootInstanceInit): RootInstance {
|
|
|
374
433
|
};
|
|
375
434
|
}
|
|
376
435
|
|
|
377
|
-
/**
|
|
378
|
-
* Type guard for whether a `MountedThreeRoot` is a first-party
|
|
379
|
-
* `SetupThreeRootAdapter` mount (has a live `GameContext` at `.ctx`). Used to
|
|
380
|
-
* decide whether a world's `physics`/`collisions`/`camera` can be populated
|
|
381
|
-
* from it — an external
|
|
382
|
-
* adapter's mount has none of these first-party handles.
|
|
383
|
-
*
|
|
384
|
-
* Checks the `firstParty: true` brand (checklist item 1), NOT `'ctx' in
|
|
385
|
-
* mounted` — a foreign mount may carry an unrelated `ctx` key, so a
|
|
386
|
-
* structural `'ctx' in mounted` check would misfire. The probe is a plain property read, not an
|
|
387
|
-
* `instanceof`/value import of `setup-three-root-adapter.ts` — the
|
|
388
|
-
* `MountedSetupThreeRoot` import above stays type-only.
|
|
389
|
-
*/
|
|
390
|
-
export function isFirstPartyMounted(mounted: MountedRoot): mounted is MountedSetupThreeRoot {
|
|
391
|
-
return (mounted as { firstParty?: unknown }).firstParty === true;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
436
|
/**
|
|
395
437
|
* One world's half of a debris disposal, as {@link disposeDebrisSubtree} needs
|
|
396
|
-
* it.
|
|
438
|
+
* it.
|
|
397
439
|
*
|
|
398
440
|
* `physics`/`rapierWorld` are OPTIONAL: an owner that builds no first-party
|
|
399
441
|
* Rapier runtime (an R3F world) carries neither, and {@link
|
|
@@ -401,8 +443,8 @@ export function isFirstPartyMounted(mounted: MountedRoot): mounted is MountedSet
|
|
|
401
443
|
* unconditionally.
|
|
402
444
|
*/
|
|
403
445
|
export interface DebrisOwner {
|
|
404
|
-
readonly physics?:
|
|
405
|
-
readonly rapierWorld?:
|
|
446
|
+
readonly physics?: PhysicsRegistry;
|
|
447
|
+
readonly rapierWorld?: import('@dimforge/rapier3d-compat').World;
|
|
406
448
|
}
|
|
407
449
|
|
|
408
450
|
/**
|
|
@@ -412,7 +454,7 @@ export interface DebrisOwner {
|
|
|
412
454
|
*/
|
|
413
455
|
function readRapierHandles(owner: DebrisOwner): {
|
|
414
456
|
physics: NonNullable<DebrisOwner['physics']>;
|
|
415
|
-
rapierWorld:
|
|
457
|
+
rapierWorld: NonNullable<DebrisOwner['rapierWorld']>;
|
|
416
458
|
} | null {
|
|
417
459
|
const { physics, rapierWorld } = owner;
|
|
418
460
|
if (!physics || !rapierWorld) return null;
|
|
@@ -424,15 +466,14 @@ function readRapierHandles(owner: DebrisOwner): {
|
|
|
424
466
|
* only *when*; this is the *what*, and it lives here because it is the runtime
|
|
425
467
|
* that knows about Rapier and shared geometry.
|
|
426
468
|
*
|
|
427
|
-
* The order mirrors `adapter/
|
|
469
|
+
* The order mirrors `adapter/world3d-react/r3f-root-factory.tsx`'s world teardown for
|
|
428
470
|
* ONE subtree. Within the physics step,
|
|
429
471
|
* `rapierWorld.removeRigidBody(body)` comes BEFORE `physics.remove(node)`: the
|
|
430
472
|
* registry's `remove()` only drops index entries, so reversing the two leaks
|
|
431
473
|
* the Rapier body.
|
|
432
474
|
*
|
|
433
|
-
* `owners` is every world that could own part of the subtree
|
|
434
|
-
*
|
|
435
|
-
* adapter mount passes its own ctx. `physics.get` is a no-op for a node the
|
|
475
|
+
* `owners` is every world that could own part of the subtree. `physics.get`
|
|
476
|
+
* is a no-op for a node the
|
|
436
477
|
* owner does not own, so offering the subtree to each is safe.
|
|
437
478
|
*
|
|
438
479
|
* Safe on an object already removed or already disposed: `removeFromParent` on
|
|
@@ -473,13 +514,17 @@ export function disposeDebrisSubtree(
|
|
|
473
514
|
}
|
|
474
515
|
|
|
475
516
|
/**
|
|
476
|
-
* The {@link DebrisOwner}s one registered world contributes
|
|
477
|
-
*
|
|
517
|
+
* The {@link DebrisOwner}s one registered world contributes. Written as a
|
|
518
|
+
* standalone function
|
|
478
519
|
* so `createGame`'s clock disposer is one line and the "which mounts count"
|
|
479
520
|
* rule has exactly one home.
|
|
480
521
|
*/
|
|
481
|
-
function debrisOwnersOf(
|
|
482
|
-
|
|
522
|
+
function debrisOwnersOf(_world: RootInstance): DebrisOwner[] {
|
|
523
|
+
// No mount carries first-party Rapier handles: physics is a declared
|
|
524
|
+
// system built inside the world's own tree, and the library owns its
|
|
525
|
+
// bodies' lifecycles there. The owner list stays as the disposal seam's
|
|
526
|
+
// shape; today it is always empty.
|
|
527
|
+
return [];
|
|
483
528
|
}
|
|
484
529
|
|
|
485
530
|
/**
|
|
@@ -499,13 +544,13 @@ export interface Game {
|
|
|
499
544
|
* Within each phase,
|
|
500
545
|
* `GameInternal.runFrame` runs THIS runner's `runPhase` first, before any
|
|
501
546
|
* world's engine systems/component ticks/world-bound game systems (e.g.
|
|
502
|
-
* `ctx.systems.add`, which stays world-bound to the default world
|
|
503
|
-
*
|
|
547
|
+
* `ctx.systems.add`, which stays world-bound to the default world).
|
|
548
|
+
* Empty for every existing game (nothing registers
|
|
504
549
|
* against it), so an empty runner has no frame cost beyond dispatch.
|
|
505
550
|
*/
|
|
506
551
|
readonly systems: SystemRunner;
|
|
507
552
|
/** Declaration-ordered. This is the same array reference `registerRoot`
|
|
508
|
-
* mutates, not a snapshot, so holders
|
|
553
|
+
* mutates, not a snapshot, so holders observe
|
|
509
554
|
* later registrations. */
|
|
510
555
|
readonly roots: ReadonlyArray<RootInstance>;
|
|
511
556
|
world(id: string): RootInstance | null;
|
|
@@ -538,6 +583,36 @@ export interface Game {
|
|
|
538
583
|
readonly systemAdapters: SystemAdapters;
|
|
539
584
|
/** Subscribe when a mounted root registers or replaces a system adapter. */
|
|
540
585
|
subscribeSystemAdapters?(listener: () => void): () => void;
|
|
586
|
+
/**
|
|
587
|
+
* Install a root's STATICALLY DECLARED system-adapter slots — the native
|
|
588
|
+
* module surface's `export const systems` (adapter/native-debug-module.ts),
|
|
589
|
+
* already validated by `contract-system-adapters.ts`. The host calls this
|
|
590
|
+
* once per declaring root at mount; project components never do
|
|
591
|
+
* (ARCHITECTURE-CORE §System adapters: "Components never call
|
|
592
|
+
* `registerSystemAdapter`" — this door is the replacement, and the slots it
|
|
593
|
+
* installs take precedence over a component registration for the same root
|
|
594
|
+
* in {@link systemAdapters}'s merge, with the standard collision warning).
|
|
595
|
+
* Throws on an unmounted root id or a second declaration for the same root.
|
|
596
|
+
* Optional for the same reason {@link subscribeSystemAdapters} is: partial
|
|
597
|
+
* `Game` doubles in tests; `installNativeSystemsBindings` refuses loudly
|
|
598
|
+
* when the hosting Game lacks it.
|
|
599
|
+
*/
|
|
600
|
+
installDeclaredSystemAdapters?(
|
|
601
|
+
rootId: string,
|
|
602
|
+
slots: Readonly<Partial<SystemAdapters>>,
|
|
603
|
+
absent?: readonly DeclaredSystemAbsence[],
|
|
604
|
+
): void;
|
|
605
|
+
/**
|
|
606
|
+
* Every slot a mounted root's `systems` table answered with `absent(reason)`,
|
|
607
|
+
* across all roots — the game-scoped read of the positive absences.
|
|
608
|
+
*
|
|
609
|
+
* This is what separates "this game has no networking" from "nobody looked",
|
|
610
|
+
* and it is the ONLY source for that distinction on a native mount: the
|
|
611
|
+
* adapter bag ({@link systemAdapters}) can only say a slot is unfilled, which
|
|
612
|
+
* is the same shape for both. Empty means no root declared any absence — NOT
|
|
613
|
+
* that every slot is answered.
|
|
614
|
+
*/
|
|
615
|
+
readonly declaredSystemAbsences?: readonly DeclaredSystemAbsence[];
|
|
541
616
|
/** The one game-owned `InputManager`, shared by every first-party root. */
|
|
542
617
|
readonly input: InputManager;
|
|
543
618
|
/**
|
|
@@ -552,7 +627,7 @@ export interface Game {
|
|
|
552
627
|
/** Game-level play-state control surface (D10, T7.6) — see {@link PlayState}. */
|
|
553
628
|
readonly play: PlayState;
|
|
554
629
|
/**
|
|
555
|
-
*
|
|
630
|
+
* The interpolation alpha the most recent display frame presented
|
|
556
631
|
* at: `accumulator / fixedDt`, in `[0, 1]`. `0` means "exactly on the last
|
|
557
632
|
* completed fixed state", `0.5` means "halfway to the next one".
|
|
558
633
|
*
|
|
@@ -564,7 +639,7 @@ export interface Game {
|
|
|
564
639
|
*/
|
|
565
640
|
readonly renderAlpha: number;
|
|
566
641
|
/**
|
|
567
|
-
*
|
|
642
|
+
* The `RenderStepped`-shaped host: register a callback that runs
|
|
568
643
|
* ONCE PER DISPLAY FRAME, before that frame's `preRender`/`render` phases,
|
|
569
644
|
* with `(alpha, displayDt)`.
|
|
570
645
|
*
|
|
@@ -639,19 +714,19 @@ export interface GameInternal extends Game {
|
|
|
639
714
|
* touches no other world at all — a natural no-op while not paused, since
|
|
640
715
|
* the frozen set is then empty.
|
|
641
716
|
*
|
|
642
|
-
*
|
|
717
|
+
* `opts.skipRenderPhases` omits `preRender`+`render` from
|
|
643
718
|
* the pass — every other phase, and `endFrame`, run exactly as always. A
|
|
644
719
|
* host that drives presentation at DISPLAY rate sets it on every substep
|
|
645
720
|
* and calls {@link runRenderFrame} once per real frame instead
|
|
646
721
|
* (`create-runtime.ts`). A host that does
|
|
647
|
-
* not set it
|
|
722
|
+
* not set it renders inside the substep — which is why
|
|
648
723
|
* `externalDrive` capture (`render-control.ts`'s `simulateSubsteps`, which
|
|
649
|
-
* calls `runFrame(fixedDt)` with no opts) is frame-exact
|
|
724
|
+
* calls `runFrame(fixedDt)` with no opts) is frame-exact.
|
|
650
725
|
* `runTicks` sets it per tick for its `render: 'none' | 'last'` modes.
|
|
651
726
|
*/
|
|
652
727
|
runFrame(dt: number, opts?: { skipRenderPhases?: boolean }): void;
|
|
653
728
|
/**
|
|
654
|
-
*
|
|
729
|
+
* Run ONE display frame's presentation pass: the registered
|
|
655
730
|
* `onRenderStep` callbacks, then the `preRender` and `render` phases across
|
|
656
731
|
* game-scoped systems and every host-driven world, in the same
|
|
657
732
|
* game-systems-then-worlds order {@link runFrame} uses.
|
|
@@ -726,8 +801,8 @@ export interface GameInternal extends Game {
|
|
|
726
801
|
runTicks(n: number, opts?: RunTicksOptions): void;
|
|
727
802
|
/**
|
|
728
803
|
* ADAPTER-MOUNT surface, not game-facing: an adapter's `mount()` calls
|
|
729
|
-
* this (via `host.game` — the
|
|
730
|
-
* `createR3FRootContext`
|
|
804
|
+
* this (via `host.game` — the R3F
|
|
805
|
+
* `createR3FRootContext` does) to load the game-owned input map ONCE,
|
|
731
806
|
* even when several roots ask for the same path; competing paths throw
|
|
732
807
|
* (input is game-owned, so roots cannot load competing maps). GAME code
|
|
733
808
|
* never calls this — a project's map loads automatically at mount from
|
|
@@ -774,7 +849,7 @@ export function createGame(opts: {
|
|
|
774
849
|
const input = new InputManager();
|
|
775
850
|
const stateBridge = createStateBridge();
|
|
776
851
|
// D15 (T-D15.1) — the game-scoped seeded-random surface every world's
|
|
777
|
-
// `ctx.random` aliases (see `
|
|
852
|
+
// `ctx.random` aliases (see `world3d-react/r3f-root-factory.tsx`'s `ctx.random =
|
|
778
853
|
// ...`, wired the same way `ctx.debug` is just below). Constructed
|
|
779
854
|
// unconditionally (cheap — a handful of closures) regardless of whether
|
|
780
855
|
// this project ever declares `determinism.seededRandom`; only the BOOT
|
|
@@ -807,7 +882,7 @@ export function createGame(opts: {
|
|
|
807
882
|
// the CURRENT values.
|
|
808
883
|
let tick = 0;
|
|
809
884
|
let simT = 0;
|
|
810
|
-
//
|
|
885
|
+
// The display-rate half. `renderAlpha` is the last alpha
|
|
811
886
|
// `runRenderFrameImpl` presented at (0 until a host drives one); the set is
|
|
812
887
|
// the `RenderStepped`-shaped registry `Game.onRenderStep` feeds. Deliberately
|
|
813
888
|
// NOT beside `tick`/`simT` in meaning: neither of these ever advances sim
|
|
@@ -823,7 +898,7 @@ export function createGame(opts: {
|
|
|
823
898
|
//
|
|
824
899
|
// The disposer is supplied HERE rather than inside the clock because
|
|
825
900
|
// `core/sim-clock.ts` deliberately knows nothing about Rapier or shared
|
|
826
|
-
// geometry. It mirrors `
|
|
901
|
+
// geometry. It mirrors `world3d-react/r3f-root-factory.tsx`'s world teardown
|
|
827
902
|
// ordering for ONE subtree.
|
|
828
903
|
const simClock: SimClockInternal = createSimClock({
|
|
829
904
|
// Every mount that could own part of the subtree is offered it:
|
|
@@ -881,7 +956,17 @@ export function createGame(opts: {
|
|
|
881
956
|
// above's "once per game instance" idiom.
|
|
882
957
|
const warnedSystemAdapterKeys = new Set<string>();
|
|
883
958
|
const systemAdapterListeners = new Set<() => void>();
|
|
884
|
-
|
|
959
|
+
/** Per-root slots installed by `installDeclaredSystemAdapters` — the native
|
|
960
|
+
* module surface's static `systems` declaration. Merged BEFORE the same
|
|
961
|
+
* root's live `mounted.systems` below, so during migration a lingering
|
|
962
|
+
* component registration for a declared slot is shadowed (with the
|
|
963
|
+
* standard warning) rather than silently winning by running later. */
|
|
964
|
+
const declaredSystemAdapters = new Map<string, Readonly<Partial<SystemAdapters>>>();
|
|
965
|
+
/** Per-root POSITIVE ABSENCES from the same declaration — `absent(reason)`
|
|
966
|
+
* slots. Nothing binds them (there is nothing to bind); they are kept so a
|
|
967
|
+
* reader can tell an answered absence from an unasked question. */
|
|
968
|
+
const declaredAbsences = new Map<string, readonly DeclaredSystemAbsence[]>();
|
|
969
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one cohesive merge-with-collision-report walk (per-world × per-source × per-key); splitting the collision-warn branch out would obscure that it's part of the same pass, not reduce real complexity
|
|
885
970
|
function computeSystemAdapters(): SystemAdapters {
|
|
886
971
|
// Debug is game-scoped: React hooks, probes, and the built-in time
|
|
887
972
|
// provider all register with the one registry created above, regardless
|
|
@@ -891,9 +976,11 @@ export function createGame(opts: {
|
|
|
891
976
|
// intentional shared registration it is.
|
|
892
977
|
const result: SystemAdapters = { debug: debugRegistry.adapter };
|
|
893
978
|
const ownerRootId = new Map<string, string>([['debug', '(game)']]);
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
979
|
+
const merge = (
|
|
980
|
+
owner: string,
|
|
981
|
+
adapters: Readonly<Partial<SystemAdapters>>,
|
|
982
|
+
quietKeys?: ReadonlySet<string>,
|
|
983
|
+
): void => {
|
|
897
984
|
for (const key of Object.keys(adapters) as (keyof SystemAdapters)[]) {
|
|
898
985
|
if (adapters[key] === undefined) continue;
|
|
899
986
|
const existingOwner = ownerRootId.get(key);
|
|
@@ -903,12 +990,19 @@ export function createGame(opts: {
|
|
|
903
990
|
// object under `systems.debug` — that is by design, not a
|
|
904
991
|
// collision, so it must never warn.
|
|
905
992
|
if (result[key] === adapters[key]) continue;
|
|
993
|
+
// A world's own DECLARED slot overriding that same world's
|
|
994
|
+
// substrate-seeded default (the three lane pre-seeds physics/audio/
|
|
995
|
+
// renderDebug into `mounted.systems`) is the door working as
|
|
996
|
+
// intended, not a collision — same silence `ctx.
|
|
997
|
+
// registerSystemAdapter` gives the same override. Cross-root
|
|
998
|
+
// collisions still warn.
|
|
999
|
+
if (quietKeys?.has(key)) continue;
|
|
906
1000
|
if (!warnedSystemAdapterKeys.has(key)) {
|
|
907
1001
|
warnedSystemAdapterKeys.add(key);
|
|
908
1002
|
// biome-ignore lint/suspicious/noConsole: structured, greppable — mirrors this file's own reportGateShortfallOnce's deliberate direct console.warn just above
|
|
909
1003
|
console.warn(
|
|
910
|
-
`[game] systemAdapters: "${key}" is registered by both
|
|
911
|
-
|
|
1004
|
+
`[game] systemAdapters: "${key}" is registered by both ${existingOwner} and ` +
|
|
1005
|
+
`${owner} — the FIRST registration (${existingOwner}) wins; the later ` +
|
|
912
1006
|
'one is shadowed (game-scoped system adapters).',
|
|
913
1007
|
);
|
|
914
1008
|
}
|
|
@@ -916,7 +1010,19 @@ export function createGame(opts: {
|
|
|
916
1010
|
}
|
|
917
1011
|
// biome-ignore lint/suspicious/noExplicitAny: SystemAdapters is a plain optional-field record; the per-key copy is correct by construction (same key on both sides), just not expressible without a cast
|
|
918
1012
|
(result as any)[key] = adapters[key];
|
|
919
|
-
ownerRootId.set(key,
|
|
1013
|
+
ownerRootId.set(key, owner);
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
for (const world of roots) {
|
|
1017
|
+
const declared = declaredSystemAdapters.get(world.id);
|
|
1018
|
+
if (declared) merge(`world "${world.id}" (declared systems export)`, declared);
|
|
1019
|
+
const adapters = world.mounted.systems;
|
|
1020
|
+
if (adapters) {
|
|
1021
|
+
merge(
|
|
1022
|
+
`world "${world.id}"`,
|
|
1023
|
+
adapters,
|
|
1024
|
+
declared ? new Set(Object.keys(declared)) : undefined,
|
|
1025
|
+
);
|
|
920
1026
|
}
|
|
921
1027
|
}
|
|
922
1028
|
return result;
|
|
@@ -952,6 +1058,13 @@ export function createGame(opts: {
|
|
|
952
1058
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: fans out TWO independent capability gates (loop, audio) with the same "call it, else report once" shape per world — splitting the two gates into separate loops would duplicate the fan-out, not reduce real complexity
|
|
953
1059
|
function setRootGates(next: boolean): void {
|
|
954
1060
|
for (const world of roots) {
|
|
1061
|
+
// A disposed world stays in `roots` (there is no `unregisterRoot`), and
|
|
1062
|
+
// its adapter's `setPaused`/`systems.audio` reach a renderer, ticker or
|
|
1063
|
+
// audio graph that `dispose()` already released — the same reason
|
|
1064
|
+
// `runFrame`/`runRenderFrame`/`runTicks` skip it. Without this, a
|
|
1065
|
+
// pause/resume after a partial teardown calls into freed resources, and
|
|
1066
|
+
// the audio leg re-mutes a context that no longer exists.
|
|
1067
|
+
if (world.disposed) continue;
|
|
955
1068
|
if (!world.pausable) continue; // pausable:false roots are untouched by design
|
|
956
1069
|
if (world.mounted.drivesOwnLoop) {
|
|
957
1070
|
if (world.mounted.setPaused) {
|
|
@@ -986,14 +1099,14 @@ export function createGame(opts: {
|
|
|
986
1099
|
// declare `determinism.seededRandom`).
|
|
987
1100
|
if (rngTrapEnabled) rngTrap.enable();
|
|
988
1101
|
profiler.beginFrame();
|
|
989
|
-
//
|
|
1102
|
+
// The render-phase skip. Two callers set it, for the
|
|
990
1103
|
// same reason — this substep is not the thing that paints. `runTicks`'s
|
|
991
1104
|
// `render: 'none'|'last'` fast-forward sets it on every tick it doesn't
|
|
992
1105
|
// want to paint (see `runTicks`'s doc comment on `GameInternal`), and a
|
|
993
1106
|
// DISPLAY-RATE host sets it on every substep because `runRenderFrame`
|
|
994
|
-
// paints once per real frame instead. Unlike `onlyFrozen` below it is
|
|
995
|
-
//
|
|
996
|
-
// signature, since an external host is
|
|
1107
|
+
// paints once per real frame instead. Unlike `onlyFrozen` below it is not
|
|
1108
|
+
// internal-only: it is part of the public `GameInternal.runFrame`
|
|
1109
|
+
// signature, since an external host is one of those two callers.
|
|
997
1110
|
const skipRenderPhases = frameOpts?.skipRenderPhases ?? false;
|
|
998
1111
|
// `onlyFrozen` is internal-only (not part of the public `GameInternal.runFrame`
|
|
999
1112
|
// signature — no external caller sets it) — `Game.play.step()` below is the
|
|
@@ -1011,6 +1124,7 @@ export function createGame(opts: {
|
|
|
1011
1124
|
inputFrameActive = false;
|
|
1012
1125
|
for (let i = 0; i < n; i++) {
|
|
1013
1126
|
const world = roots[i]!;
|
|
1127
|
+
if (world.disposed) continue;
|
|
1014
1128
|
if (onlyFrozen) {
|
|
1015
1129
|
if (paused && world.pausable && !world.mounted.drivesOwnLoop) {
|
|
1016
1130
|
inputFrameActive = true;
|
|
@@ -1040,6 +1154,7 @@ export function createGame(opts: {
|
|
|
1040
1154
|
if (!onlyFrozen) {
|
|
1041
1155
|
for (let i = 0; i < n; i++) {
|
|
1042
1156
|
const world = roots[i]!;
|
|
1157
|
+
if (world.disposed) continue;
|
|
1043
1158
|
if (!world.mounted.drivesOwnLoop) continue;
|
|
1044
1159
|
if (!paused || !world.pausable || !world.mounted.setPaused) {
|
|
1045
1160
|
advanced = true;
|
|
@@ -1064,6 +1179,7 @@ export function createGame(opts: {
|
|
|
1064
1179
|
if (inputFrameActive) systems.runPhase(phase, dt);
|
|
1065
1180
|
for (let i = 0; i < n; i++) {
|
|
1066
1181
|
const world = roots[i]!;
|
|
1182
|
+
if (world.disposed) continue;
|
|
1067
1183
|
if (world.mounted.drivesOwnLoop) continue;
|
|
1068
1184
|
if (!(paused && world.pausable)) continue; // only the frozen set
|
|
1069
1185
|
try {
|
|
@@ -1080,6 +1196,7 @@ export function createGame(opts: {
|
|
|
1080
1196
|
}
|
|
1081
1197
|
for (let i = 0; i < n; i++) {
|
|
1082
1198
|
const world = roots[i]!;
|
|
1199
|
+
if (world.disposed) continue;
|
|
1083
1200
|
if (world.mounted.drivesOwnLoop) continue;
|
|
1084
1201
|
if (!(paused && world.pausable)) continue;
|
|
1085
1202
|
advanced = true;
|
|
@@ -1116,6 +1233,7 @@ export function createGame(opts: {
|
|
|
1116
1233
|
systems.runPhase(phase, dt);
|
|
1117
1234
|
for (let i = 0; i < n; i++) {
|
|
1118
1235
|
const world = roots[i]!;
|
|
1236
|
+
if (world.disposed) continue;
|
|
1119
1237
|
if (world.mounted.drivesOwnLoop) continue;
|
|
1120
1238
|
// D10/T7.6: a `pausable` world under an active pause
|
|
1121
1239
|
// skips every phase except `render` — its render still runs, every
|
|
@@ -1143,6 +1261,7 @@ export function createGame(opts: {
|
|
|
1143
1261
|
}
|
|
1144
1262
|
for (let i = 0; i < n; i++) {
|
|
1145
1263
|
const world = roots[i]!;
|
|
1264
|
+
if (world.disposed) continue;
|
|
1146
1265
|
if (world.mounted.drivesOwnLoop) continue;
|
|
1147
1266
|
// A fully-frozen world gets no `endFrame`/opaque-`update` call either —
|
|
1148
1267
|
// there is nothing to "end the frame" of when nothing ran this substep.
|
|
@@ -1196,7 +1315,7 @@ export function createGame(opts: {
|
|
|
1196
1315
|
}
|
|
1197
1316
|
|
|
1198
1317
|
/**
|
|
1199
|
-
*
|
|
1318
|
+
* One display frame's PRESENTATION pass. See
|
|
1200
1319
|
* `GameInternal.runRenderFrame`'s doc comment for the contract; this is the
|
|
1201
1320
|
* `preRender`+`render` slice of `runFrameImpl`'s phase loop, lifted out and
|
|
1202
1321
|
* driven by the loop's own per-real-frame callback instead of by the substep
|
|
@@ -1244,6 +1363,7 @@ export function createGame(opts: {
|
|
|
1244
1363
|
systems.runPhase(phase, displayDt);
|
|
1245
1364
|
for (let i = 0; i < n; i++) {
|
|
1246
1365
|
const world = roots[i]!;
|
|
1366
|
+
if (world.disposed) continue;
|
|
1247
1367
|
if (world.mounted.drivesOwnLoop) continue;
|
|
1248
1368
|
// D10/T7.6, carried over verbatim in substance: a frozen world still
|
|
1249
1369
|
// renders (a paused viewport stays painted) but with `dt` forced to
|
|
@@ -1292,6 +1412,34 @@ export function createGame(opts: {
|
|
|
1292
1412
|
notifySystemAdaptersChanged() {
|
|
1293
1413
|
for (const listener of systemAdapterListeners) listener();
|
|
1294
1414
|
},
|
|
1415
|
+
get declaredSystemAbsences() {
|
|
1416
|
+
return [...declaredAbsences.values()].flat();
|
|
1417
|
+
},
|
|
1418
|
+
installDeclaredSystemAdapters(
|
|
1419
|
+
rootId: string,
|
|
1420
|
+
slots: Readonly<Partial<SystemAdapters>>,
|
|
1421
|
+
absent: readonly DeclaredSystemAbsence[] = [],
|
|
1422
|
+
) {
|
|
1423
|
+
if (!roots.some((w) => w.id === rootId)) {
|
|
1424
|
+
throw new Error(
|
|
1425
|
+
`installDeclaredSystemAdapters: no mounted root is named "${rootId}" ` +
|
|
1426
|
+
`(mounted: ${roots.map((w) => `"${w.id}"`).join(', ') || 'none'}).`,
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
if (declaredSystemAdapters.has(rootId)) {
|
|
1430
|
+
throw new Error(
|
|
1431
|
+
`installDeclaredSystemAdapters: root "${rootId}" already installed its declared ` +
|
|
1432
|
+
'systems — a declaration is static and installs exactly once per mount.',
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
declaredSystemAdapters.set(rootId, slots);
|
|
1436
|
+
// ALWAYS set, never merge: a re-install (an HMR of the entry's `systems`
|
|
1437
|
+
// table) that dropped an `absent()` marker must also drop the recorded
|
|
1438
|
+
// absence, or the coverage report keeps printing a game statement the
|
|
1439
|
+
// game no longer makes.
|
|
1440
|
+
declaredAbsences.set(rootId, absent);
|
|
1441
|
+
gameInternal.notifySystemAdaptersChanged();
|
|
1442
|
+
},
|
|
1295
1443
|
input,
|
|
1296
1444
|
state: stateBridge,
|
|
1297
1445
|
play: {
|
|
@@ -1318,6 +1466,10 @@ export function createGame(opts: {
|
|
|
1318
1466
|
// not paused, nothing here runs — step() is a whole-call no-op.
|
|
1319
1467
|
if (paused) {
|
|
1320
1468
|
for (const world of roots) {
|
|
1469
|
+
// Same disposed-world guard `runFrameImpl` (and `setRootGates`)
|
|
1470
|
+
// make: a disposed world's `mounted.step` drives a loop whose
|
|
1471
|
+
// resources are already freed.
|
|
1472
|
+
if (world.disposed) continue;
|
|
1321
1473
|
if (!world.pausable || !world.mounted.drivesOwnLoop) continue;
|
|
1322
1474
|
if (!world.mounted.setPaused) continue; // never gated — still running
|
|
1323
1475
|
if (world.mounted.step) {
|
|
@@ -1344,8 +1496,8 @@ export function createGame(opts: {
|
|
|
1344
1496
|
// `endFrame` called once per registration) and double-dispose-wrapped
|
|
1345
1497
|
// (`createRootInstance` wraps `mounted.dispose` in place — a second
|
|
1346
1498
|
// wrap would flip `disposed` and call through on ITS OWN wrapped
|
|
1347
|
-
// `originalDispose`, which
|
|
1348
|
-
//
|
|
1499
|
+
// `originalDispose`, which would be harmless only if the adapter's
|
|
1500
|
+
// dispose happened to be idempotent; a foreign adapter
|
|
1349
1501
|
// has no such guarantee). Reject it outright instead.
|
|
1350
1502
|
if (roots.some((w) => w.mounted === world.mounted)) {
|
|
1351
1503
|
throw new Error(
|
|
@@ -1357,14 +1509,14 @@ export function createGame(opts: {
|
|
|
1357
1509
|
roots.push(world);
|
|
1358
1510
|
gameInternal.notifySystemAdaptersChanged();
|
|
1359
1511
|
// Notify state-bridge subscribers THE INSTANT the world list changes —
|
|
1360
|
-
// not just at the next completed `runFrame`.
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1512
|
+
// not just at the next completed `runFrame`. Otherwise a UI that lists
|
|
1513
|
+
// worlds hangs on an empty list: `mountAllRootSpecs`
|
|
1514
|
+
// (`create-runtime.ts`) mounts roots
|
|
1363
1515
|
// SEQUENTIALLY, and a react world's `adapter.mount()`
|
|
1364
1516
|
// (`resolveDefaultReactAdapter`/`mountOneReactRoot`) renders its tree —
|
|
1365
1517
|
// synchronously in some React builds, but React 19's concurrent
|
|
1366
1518
|
// renderer does NOT guarantee a synchronous first commit (see
|
|
1367
|
-
// `r3f-
|
|
1519
|
+
// `r3f-root-factory.tsx`'s own doc comment on `onCreated`) — BEFORE the
|
|
1368
1520
|
// caller calls `registerRoot` for that very world. A `useWorldState`
|
|
1369
1521
|
// selector reading `g.roots` can therefore render for the first time
|
|
1370
1522
|
// while `roots` is still missing entries that register moments later.
|
|
@@ -1408,17 +1560,11 @@ export function createGame(opts: {
|
|
|
1408
1560
|
// world mount), eroding the signal for a genuinely un-observable
|
|
1409
1561
|
// ingested world.
|
|
1410
1562
|
// A HOST-DRIVEN mount (`drivesOwnLoop: false`, notably the R3F adapter)
|
|
1411
|
-
// is observable through `Game.state`
|
|
1412
|
-
//
|
|
1413
|
-
// `runFrame`, so the frame-versioned bridge covers it. Requiring a
|
|
1563
|
+
// is observable through `Game.state` when it ticks inside
|
|
1564
|
+
// `runFrame` — the frame-versioned bridge covers it. Requiring a
|
|
1414
1565
|
// foreign-world `observe` bridge there produces a false warning while
|
|
1415
1566
|
// the canonical bridge is already live.
|
|
1416
|
-
if (
|
|
1417
|
-
!isFirstPartyMounted(world.mounted) &&
|
|
1418
|
-
world.mounted.drivesOwnLoop &&
|
|
1419
|
-
world.kind !== 'dom' &&
|
|
1420
|
-
!world.mounted.observe
|
|
1421
|
-
) {
|
|
1567
|
+
if (world.mounted.drivesOwnLoop && world.kind !== 'dom' && !world.mounted.observe) {
|
|
1422
1568
|
console.warn(
|
|
1423
1569
|
`[game] world "${world.id}" (kind: ${world.kind}, adapter: "${world.adapter.id}"): ` +
|
|
1424
1570
|
'no state bridge — this mounted game has no `observe` (RootStateObserver); ' +
|
|
@@ -1514,7 +1660,7 @@ export function createGame(opts: {
|
|
|
1514
1660
|
// root's `mounted.dispose()`, whereas a per-root teardown may be ending
|
|
1515
1661
|
// just one sub-session while sibling roots keep running (see the
|
|
1516
1662
|
// disposed-world guard in `runFrame`). It used to be disposed from
|
|
1517
|
-
// `
|
|
1663
|
+
// `world3d-react/r3f-root-factory.tsx`'s teardown, which meant disposing one of
|
|
1518
1664
|
// two three roots froze `now()` for the whole Game, rejected the other
|
|
1519
1665
|
// world's pending `delay`s and turned its `after()` calls into silent
|
|
1520
1666
|
// no-ops. Note the asymmetry that gives the bug away: `seededRandom` and
|