@vgai/engine 0.5.1 → 0.5.3
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 +18 -11
- package/package.json +10 -6
- package/schemas/engine-api.json +1 -68
- package/schemas/engine-api.md +1 -32
- package/schemas/engine-capabilities.json +30 -42
- package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
- package/src/adapter/adapter-surface.ts +5 -5
- package/src/adapter/authoring.ts +168 -226
- package/src/adapter/colyseus-networking-adapter.ts +44 -5
- package/src/adapter/first-party-systems.ts +156 -42
- package/src/adapter/host-context.ts +177 -46
- package/src/adapter/index.ts +47 -51
- package/src/adapter/ingest/game-contract.ts +2 -2
- package/src/adapter/ingest/scene-capture.ts +18 -19
- package/src/adapter/ingest/structural-ids.ts +127 -0
- package/src/adapter/ingest/upstream-pin.ts +9 -12
- package/src/adapter/loop-gate-report.ts +11 -11
- package/src/adapter/rapier-physics-adapter.ts +27 -9
- package/src/adapter/root-adapter.ts +217 -0
- package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
- package/src/adapter/system-adapter.ts +80 -63
- package/src/ai/navigation.ts +1 -1
- package/src/animation/animation-clock.ts +1 -1
- package/src/animation/camera-ownership.ts +1 -2
- package/src/animation/cubic-spline-interpolant.ts +132 -0
- package/src/animation/theatre-clock-binding.ts +2 -2
- package/src/animation/theatre-object-binding.ts +4 -4
- package/src/animation/xstate-animation-binding.ts +75 -5
- package/src/{scene/schema → asset-formats}/camera.ts +2 -5
- package/src/{scene/schema → asset-formats}/collider.ts +2 -5
- package/src/asset-formats/index.ts +54 -0
- package/src/{scene/schema → asset-formats}/instances.ts +10 -6
- package/src/{scene/schema → asset-formats}/light.ts +3 -6
- package/src/{scene/schema → asset-formats}/material.ts +4 -7
- package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
- package/src/asset-formats/parse.ts +39 -0
- package/src/{scene/schema → asset-formats}/particles.ts +3 -6
- package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
- package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
- package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
- package/src/asset-parse-error.ts +33 -0
- package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
- package/src/assets.ts +1 -1
- package/src/audio/wav-encode.ts +9 -9
- package/src/canvas-react/engine-bridge.ts +59 -0
- package/src/canvas-react/index.ts +50 -0
- package/src/canvas-react/pixi-primitive.tsx +202 -0
- package/src/canvas-react/pixi-react-adapter.tsx +290 -0
- package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
- package/src/canvas-react/world-context.ts +328 -0
- package/src/core/frame-pacing.ts +100 -0
- package/src/core/game-loop.ts +50 -28
- package/src/core/seeded-random.ts +7 -7
- package/src/core/sim-clock.ts +388 -0
- package/src/core/system-runner.ts +17 -63
- package/src/core/types.ts +34 -15
- package/src/data/data-asset.ts +3 -3
- package/src/data/data-check-core.ts +6 -7
- package/src/data/data-ref.ts +11 -11
- package/src/data/vite-plugin-data.ts +10 -10
- package/src/{scene/defaults.ts → defaults.ts} +18 -40
- package/src/dev/render-debug-adapter.ts +1 -1
- package/src/dev/webgl-frame-capture.ts +1 -1
- package/src/ecs/scene-index.ts +439 -0
- package/src/ecs/scene-query.ts +43 -0
- package/src/{scene → ecs}/user-data.ts +17 -36
- package/src/index.ts +7 -9
- package/src/input/input-manager.ts +29 -32
- package/src/input/input-types.ts +2 -2
- package/src/input/schema.ts +5 -5
- package/src/loader.ts +57 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +49 -0
- package/src/manifest/index.ts +8 -2
- package/src/manifest/load-file.ts +11 -0
- package/src/manifest/load.ts +65 -77
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/schema.ts +400 -233
- package/src/{scene → physics}/collider-dimensions.ts +3 -3
- package/src/physics/physics-registry.ts +1 -1
- package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
- package/src/pixi/index.ts +43 -0
- package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
- package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
- package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
- package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
- package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
- package/src/react/unmanaged-root-detector.ts +26 -1
- package/src/react/use-data.ts +5 -5
- package/src/react/use-selection.tsx +15 -42
- package/src/react/{game-state.tsx → world-state.tsx} +44 -46
- package/src/render/auto-batcher.ts +1 -2
- package/src/{scene → render}/instance-mesh.ts +1 -1
- package/src/{scene → render}/light-camera-factory.ts +14 -13
- package/src/render/lod.ts +17 -0
- package/src/{scene → render}/material-factory.ts +8 -7
- package/src/{scene → render}/particles-factory.ts +62 -12
- package/src/render/render-batch-system.ts +14 -41
- package/src/render/render-features.ts +1 -1
- package/src/render/render-settings.ts +1 -2
- package/src/render/spark-renderer-lifecycle.ts +1 -1
- package/src/runtime/create-runtime.ts +352 -463
- package/src/runtime/debug-bridge.ts +148 -93
- package/src/runtime/debug-registry.ts +79 -54
- package/src/runtime/dev-layers.ts +40 -0
- package/src/runtime/frame-selector-cache.ts +4 -4
- package/src/runtime/game.ts +618 -369
- package/src/runtime/gameplay-rng-trap.ts +6 -7
- package/src/runtime/input-router.ts +11 -11
- package/src/runtime/mount-game.ts +54 -55
- package/src/runtime/mount-manifest.ts +154 -150
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-audio-control.ts +64 -53
- package/src/runtime/render-control.ts +45 -51
- package/src/runtime/render-seed.ts +3 -4
- package/src/runtime/state-bridge.ts +17 -18
- package/src/runtime/types.ts +94 -61
- package/src/setup/setup-renderer.ts +11 -6
- package/src/world3d-react/engine-bridge.ts +46 -33
- package/src/world3d-react/index.ts +31 -26
- package/src/world3d-react/r3f-adapter.tsx +211 -96
- package/src/world3d-react/r3f-root-factory.tsx +91 -0
- package/src/world3d-react/renderer-config.ts +137 -0
- package/src/world3d-react/world-context.ts +141 -111
- package/schemas/entity2d.schema.json +0 -468
- package/schemas/prefab.schema.json +0 -9992
- package/schemas/scn2d.schema.json +0 -494
- package/schemas/vscn.schema.json +0 -10851
- package/src/adapter/game-adapter.ts +0 -164
- package/src/adapter/ingest/overlay-applier.ts +0 -207
- package/src/adapter/ingest/overlay-apply.ts +0 -168
- package/src/adapter/ingest/overlay-file.ts +0 -126
- package/src/adapter/ingest/overlay-report.ts +0 -176
- package/src/animation/gsap-registration.ts +0 -184
- package/src/audio/audio-introspection.ts +0 -290
- package/src/audio/index.ts +0 -39
- package/src/audio/tone-clock-binding.ts +0 -98
- package/src/audio/tone-context.ts +0 -175
- package/src/audio/tone-offline-render.ts +0 -167
- package/src/ecs/component-manager.ts +0 -814
- package/src/ecs/game-component.ts +0 -260
- package/src/ecs/hmr-swap-report.ts +0 -65
- package/src/physics/trigger-dispatch.ts +0 -97
- package/src/react/root-adapter.tsx +0 -49
- package/src/scene/asset-paths.ts +0 -121
- package/src/scene/asset-ref-check.ts +0 -248
- package/src/scene/component-registry.ts +0 -51
- package/src/scene/parse.ts +0 -204
- package/src/scene/scene-apply.ts +0 -407
- package/src/scene/scene-diff-schema.ts +0 -115
- package/src/scene/scene-diff-types.ts +0 -29
- package/src/scene/scene-loader.ts +0 -1526
- package/src/scene/scene-query.ts +0 -63
- package/src/scene/scene-types.ts +0 -33
- package/src/scene/scene-version.ts +0 -40
- package/src/scene/schema/animation.ts +0 -46
- package/src/scene/schema/audio.ts +0 -25
- package/src/scene/schema/entity-ref.ts +0 -78
- package/src/scene/schema/entity.ts +0 -189
- package/src/scene/schema/index.ts +0 -51
- package/src/scene/schema/joint.ts +0 -26
- package/src/scene/schema/physics.ts +0 -49
- package/src/scene/schema/scene-file.ts +0 -292
- package/src/scene/schema/shadow.ts +0 -24
- package/src/scene/schema/spline.ts +0 -21
- package/src/world2d/asset-paths2d.ts +0 -44
- package/src/world2d/capture-to-scene2d.ts +0 -52
- package/src/world2d/collision-2d.ts +0 -99
- package/src/world2d/entity2d-asset.ts +0 -22
- package/src/world2d/index.ts +0 -91
- package/src/world2d/physics2d-transform.ts +0 -173
- package/src/world2d/physics2d-units.ts +0 -10
- package/src/world2d/pixi-game-adapter.ts +0 -439
- package/src/world2d/pixi-surface.ts +0 -78
- package/src/world2d/scene2d-identity.ts +0 -49
- package/src/world2d/scene2d-loader.ts +0 -433
- package/src/world2d/schema/entity2d.ts +0 -163
- package/src/world2d/schema/physics2d.ts +0 -64
- package/src/world2d/schema/sprite.ts +0 -99
- package/src/world2d/schema/tilemap.ts +0 -39
- package/src/world2d/schema/tuples2d.ts +0 -25
- package/src/world2d/transform-writer-2d.ts +0 -42
- package/src/world2d/types.ts +0 -74
- package/src/world3d-react/behavior.tsx +0 -146
- /package/src/{scene → render}/mesh-shadow.ts +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* world3d-react/renderer-config.ts — the seam a WORLD uses to state how its own frame is rendered.
|
|
3
|
+
*
|
|
4
|
+
* The host owns the `WebGLRenderer` (`ThreeHostContext.renderer`) and configures it with this
|
|
5
|
+
* engine's defaults: ACES tone mapping, sRGB output, PCF-soft shadows. Those defaults are right for
|
|
6
|
+
* a world authored against them and WRONG for a world that was authored against a different
|
|
7
|
+
* engine's pipeline — an imported Godot 3 GLES2 game does gamma-space lighting with no tonemapper
|
|
8
|
+
* at all, so ACES quietly desaturates and darkens every colour its author picked.
|
|
9
|
+
*
|
|
10
|
+
* So a world may DECLARE the pipeline it was authored for, and `createR3FAdapter` applies it to the
|
|
11
|
+
* host's renderer for the life of the mount, restoring what it found on dispose. Three properties
|
|
12
|
+
* of that shape are load-bearing:
|
|
13
|
+
*
|
|
14
|
+
* - **It is per-renderer, never engine-wide.** Every field here is a `WebGLRenderer` instance
|
|
15
|
+
* property, and a play root gets its own renderer (`create-runtime.ts`). Nothing here reaches a
|
|
16
|
+
* module-level three global, so one world's declaration cannot change how the editor's own
|
|
17
|
+
* viewport, another root, or a thumbnail bake renders.
|
|
18
|
+
* - **Absent means "leave the host's value alone".** Every field is optional and an omitted one
|
|
19
|
+
* is never written, so declaring a tone mapping does not silently reset the clear colour.
|
|
20
|
+
* - **It is restored on dispose.** The renderer outlives the mount, so a world that did not put
|
|
21
|
+
* back what it found would leak its pipeline into whatever mounts next.
|
|
22
|
+
*
|
|
23
|
+
* This is deliberately NOT a general render-settings system. It carries what a world can honestly
|
|
24
|
+
* state about its own colour pipeline and nothing else; a property the host fixes at CONSTRUCTION
|
|
25
|
+
* (the WebGL context's `antialias` attribute, and therefore the MSAA sample count) cannot be
|
|
26
|
+
* declared here, because there would be no honest moment to apply it.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type * as THREE from 'three';
|
|
30
|
+
|
|
31
|
+
/** The tone-mapping operators three exposes, named as data rather than as three's numeric enum. */
|
|
32
|
+
export type WorldToneMapping =
|
|
33
|
+
| 'none'
|
|
34
|
+
| 'linear'
|
|
35
|
+
| 'reinhard'
|
|
36
|
+
| 'cineon'
|
|
37
|
+
| 'aces'
|
|
38
|
+
| 'agx'
|
|
39
|
+
| 'neutral';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The output transfer function the frame is written with.
|
|
43
|
+
*
|
|
44
|
+
* - `srgb` — three's own default and this engine's: linear lighting, sRGB encode on output.
|
|
45
|
+
* - `srgb-linear` — NO output transform. This is what a gamma-space renderer needs: the shading
|
|
46
|
+
* result is already in display space and encoding it a second time washes the frame out.
|
|
47
|
+
*/
|
|
48
|
+
export type WorldOutputColorSpace = 'srgb' | 'srgb-linear';
|
|
49
|
+
|
|
50
|
+
/** What a world may declare about the renderer that draws it. Every field is optional; see header. */
|
|
51
|
+
export interface WorldRendererConfig {
|
|
52
|
+
readonly toneMapping?: WorldToneMapping | undefined;
|
|
53
|
+
readonly toneMappingExposure?: number | undefined;
|
|
54
|
+
readonly outputColorSpace?: WorldOutputColorSpace | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* The colour the frame is cleared to, as a CSS hex string. The renderer's existing clear ALPHA
|
|
57
|
+
* is preserved: a stacked canvas is transparent on purpose (`create-runtime.ts` gives every
|
|
58
|
+
* non-bottom root `alpha: true`), and forcing it opaque here would hide every layer below.
|
|
59
|
+
*/
|
|
60
|
+
readonly clearColor?: string | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Apply `config` to `renderer`, returning the restore function that puts back what was there.
|
|
65
|
+
*
|
|
66
|
+
* `three` is passed in rather than imported for values so the enum constants come from the HOST's
|
|
67
|
+
* three instance — the same identity rule `r3f-adapter.tsx` follows for the scene and camera.
|
|
68
|
+
*/
|
|
69
|
+
export function applyWorldRendererConfig(
|
|
70
|
+
three: typeof THREE,
|
|
71
|
+
renderer: THREE.WebGLRenderer,
|
|
72
|
+
config: WorldRendererConfig,
|
|
73
|
+
): () => void {
|
|
74
|
+
// A host that mounts a world WITHOUT rasterizing it hands the adapter a duck-typed renderer —
|
|
75
|
+
// the editor's design session (`createDesignTimeRenderer`: four members, deliberately never
|
|
76
|
+
// widened) and jsdom test harnesses both do. Such a surface has no colour pipeline to configure:
|
|
77
|
+
// the frame the user sees is drawn by a DIFFERENT renderer (the editor's own), so applying the
|
|
78
|
+
// world's config there is meaningless — and calling `getClearColor` on it is a TypeError that
|
|
79
|
+
// unmounts the whole world at edit time (measured: every Godot port's edit viewport blanked with
|
|
80
|
+
// '"world" failed to mount — renderer.getClearColor is not a function'). Detect the real
|
|
81
|
+
// `WebGLRenderer` surface by the one method this function must call, and no-op otherwise.
|
|
82
|
+
if (typeof renderer.getClearColor !== 'function') {
|
|
83
|
+
return () => {};
|
|
84
|
+
}
|
|
85
|
+
const toneMappings: Record<WorldToneMapping, THREE.ToneMapping> = {
|
|
86
|
+
none: three.NoToneMapping,
|
|
87
|
+
linear: three.LinearToneMapping,
|
|
88
|
+
reinhard: three.ReinhardToneMapping,
|
|
89
|
+
cineon: three.CineonToneMapping,
|
|
90
|
+
aces: three.ACESFilmicToneMapping,
|
|
91
|
+
agx: three.AgXToneMapping,
|
|
92
|
+
neutral: three.NeutralToneMapping,
|
|
93
|
+
};
|
|
94
|
+
const colorSpaces: Record<WorldOutputColorSpace, THREE.ColorSpace> = {
|
|
95
|
+
srgb: three.SRGBColorSpace,
|
|
96
|
+
'srgb-linear': three.LinearSRGBColorSpace,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const restores: (() => void)[] = [];
|
|
100
|
+
|
|
101
|
+
if (config.toneMapping !== undefined) {
|
|
102
|
+
const previous = renderer.toneMapping;
|
|
103
|
+
renderer.toneMapping = toneMappings[config.toneMapping];
|
|
104
|
+
restores.push(() => {
|
|
105
|
+
renderer.toneMapping = previous;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (config.toneMappingExposure !== undefined) {
|
|
109
|
+
const previous = renderer.toneMappingExposure;
|
|
110
|
+
renderer.toneMappingExposure = config.toneMappingExposure;
|
|
111
|
+
restores.push(() => {
|
|
112
|
+
renderer.toneMappingExposure = previous;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (config.outputColorSpace !== undefined) {
|
|
116
|
+
const previous = renderer.outputColorSpace;
|
|
117
|
+
renderer.outputColorSpace = colorSpaces[config.outputColorSpace];
|
|
118
|
+
restores.push(() => {
|
|
119
|
+
renderer.outputColorSpace = previous;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (config.clearColor !== undefined) {
|
|
123
|
+
const previousColor = new three.Color();
|
|
124
|
+
renderer.getClearColor(previousColor);
|
|
125
|
+
// Alpha is READ BACK and re-passed, never assumed: see `clearColor`'s doc above.
|
|
126
|
+
const alpha = renderer.getClearAlpha();
|
|
127
|
+
renderer.setClearColor(new three.Color(config.clearColor), alpha);
|
|
128
|
+
restores.push(() => {
|
|
129
|
+
renderer.setClearColor(previousColor, alpha);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
// Reverse order, so a field written twice (it cannot be, today) unwinds correctly.
|
|
135
|
+
for (let i = restores.length - 1; i >= 0; i--) restores[i]?.();
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
2
|
+
* `createR3FRootContext` — the ENGINE runtime an R3F world owns, split out of
|
|
3
3
|
* `./r3f-adapter.tsx` so it stays react-free and headlessly unit-testable
|
|
4
4
|
* (`test/world3d-react-context-parity.test.tsx`).
|
|
5
5
|
*
|
|
6
6
|
* Context parity with the classic first-party adapter
|
|
7
|
-
* (`../adapter/
|
|
7
|
+
* (`../adapter/setup-three-root-adapter.ts`) — closed here after a dogfood
|
|
8
8
|
* build had to shim around three silent gaps:
|
|
9
9
|
*
|
|
10
10
|
* 1. **`ctx.debug` / `ctx.random` / `ctx.game` / `ctx.roots` are wired**
|
|
11
11
|
* exactly the way the classic adapter wires them: the ONE game-scoped
|
|
12
12
|
* debug registry (`getDebugRegistry(host.game)`) provides
|
|
13
|
-
* `ctx.debug = registry.
|
|
14
|
-
*
|
|
15
|
-
* to `vgai
|
|
13
|
+
* `ctx.debug = registry.forRoot(id)`, so a component's
|
|
14
|
+
* effect can `ctx.debug.registerStateProvider(...)` and be visible
|
|
15
|
+
* to `vgai eval`/the editor's debug panels — previously a silent no-op.
|
|
16
16
|
* 2. **`ctx.input` is the game-owned `InputManager`** (`host.game.input` —
|
|
17
17
|
* the same instance the classic adapter uses when hosted), and this
|
|
18
18
|
* module registers the SAME per-world debug-registry seams the classic
|
|
@@ -25,40 +25,89 @@
|
|
|
25
25
|
* LOUDLY (console.error naming the path and what breaks) without
|
|
26
26
|
* failing the mount — an R3F tree with no actions (e.g. a pure
|
|
27
27
|
* OrbitControls demo, `examples/r3f-first-party`) is legal.
|
|
28
|
-
* 3. **Absent subsystems
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* The `ComponentManager` built here is also exposed on the mounted world
|
|
35
|
-
* (`MountedWorldBase.components`) so `Game.queryByComponent` aggregates R3F
|
|
36
|
-
* Behavior components exactly like scene-authored ones — see
|
|
37
|
-
* `runtime/game.ts`.
|
|
38
|
-
*/
|
|
28
|
+
* 3. **Absent subsystems are absent from the TYPE.** An R3F world builds no
|
|
29
|
+
* Rapier world, composer, audio, particles, or debug-draw, and
|
|
30
|
+
* {@link R3FGameContext} — the type `useGameContext()` hands back — simply
|
|
31
|
+
* does not have those fields. Reaching for one is a compile error at the
|
|
32
|
+
* call site, with the sanctioned alternative in that type's doc comment.
|
|
33
|
+
* */
|
|
39
34
|
|
|
40
35
|
import type * as THREE from 'three';
|
|
41
|
-
import type {
|
|
36
|
+
import type { ThreeHostContext } from '../adapter';
|
|
42
37
|
import type { SystemAdapters } from '../adapter/system-adapter';
|
|
43
38
|
import {
|
|
44
39
|
createSeededRandom,
|
|
45
40
|
DEFAULT_SEEDED_RANDOM_SEED,
|
|
46
41
|
getSeededRandom,
|
|
47
42
|
} from '../core/seeded-random';
|
|
43
|
+
import { createSimClock, getSimClock, type SimClockInternal } from '../core/sim-clock';
|
|
48
44
|
import { createSystemRunner } from '../core/system-runner';
|
|
49
|
-
import { type
|
|
45
|
+
import { createSceneIndex, type SceneIndex } from '../ecs/scene-index';
|
|
50
46
|
import {
|
|
51
47
|
createDebugRegistry,
|
|
52
48
|
type DebugRegistry,
|
|
53
49
|
getDebugRegistry,
|
|
54
50
|
} from '../runtime/debug-registry';
|
|
51
|
+
import { disposeDebrisSubtree } from '../runtime/game';
|
|
55
52
|
import type { GameContext } from '../runtime/types';
|
|
56
53
|
|
|
57
54
|
/** The classic adapter's default input-map path — the conventional location
|
|
58
55
|
* every scaffolded project ships (`public/inputmaps/default.inputmap.json`). */
|
|
59
56
|
export const DEFAULT_INPUT_MAP_PATH = '/inputmaps/default.inputmap.json';
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The engine context an R3F world genuinely provides — what
|
|
60
|
+
* `useGameContext()` hands back.
|
|
61
|
+
*
|
|
62
|
+
* An EXPLICIT `Pick`, never an `Omit`: a field added to `GameContext` later
|
|
63
|
+
* must be deliberately admitted here, so this surface can never silently widen
|
|
64
|
+
* into something an R3F mount does not build.
|
|
65
|
+
*
|
|
66
|
+
* **What an R3F world does NOT have, and what to reach for instead.** These
|
|
67
|
+
* are not oversights — the mount builds none of them, so they are absent from
|
|
68
|
+
* this type and naming one is a compile error rather than a crash mid-render:
|
|
69
|
+
*
|
|
70
|
+
* - **Physics** (`rapierWorld`, `rapier`, `physics`, `collisions`) — an R3F
|
|
71
|
+
* world does not build the first-party Rapier runtime. Use a physics
|
|
72
|
+
* solution inside the fiber tree (e.g. `@react-three/rapier`), or an
|
|
73
|
+
* imperative three root whose `setup(ctx)` receives the first-party Rapier
|
|
74
|
+
* runtime.
|
|
75
|
+
* - **`composer`** — R3F renders through react-three-fiber against the host
|
|
76
|
+
* renderer; there is no postprocessing `EffectComposer` here. Use
|
|
77
|
+
* fiber-native postprocessing inside the tree.
|
|
78
|
+
* - **`audio`** — no first-party audio context is built. Own a Web Audio graph
|
|
79
|
+
* inside the tree (or use drei's audio helpers) and register it as this
|
|
80
|
+
* world's `SystemAdapters.audio` via
|
|
81
|
+
* `ctx.registerSystemAdapter('audio', …)`, so host mute/pause can still
|
|
82
|
+
* silence it — `examples/third-person/src/lib/audio/sfx.ts` is the worked
|
|
83
|
+
* reference.
|
|
84
|
+
* - **`particles`** — no three.quarks particle runtime. Drive particles from
|
|
85
|
+
* the fiber tree.
|
|
86
|
+
* - **`debugDraw`** — no debug-draw helper. Add helper objects to the fiber
|
|
87
|
+
* tree directly.
|
|
88
|
+
*
|
|
89
|
+
* For the full classic runtime, use an imperative three root instead: a module
|
|
90
|
+
* exporting `setup(ctx): Promise<GameCleanup>`, which `SetupThreeRootAdapter`
|
|
91
|
+
* mounts with the complete {@link GameContext}.
|
|
92
|
+
*/
|
|
93
|
+
export type R3FGameContext = Pick<
|
|
94
|
+
GameContext,
|
|
95
|
+
| 'scene'
|
|
96
|
+
| 'camera'
|
|
97
|
+
| 'sceneIndex'
|
|
98
|
+
| 'input'
|
|
99
|
+
| 'assets'
|
|
100
|
+
| 'systems'
|
|
101
|
+
| 'clock'
|
|
102
|
+
| 'debug'
|
|
103
|
+
| 'random'
|
|
104
|
+
| 'game'
|
|
105
|
+
| 'roots'
|
|
106
|
+
| 'playtest'
|
|
107
|
+
| 'registerSystemAdapter'
|
|
108
|
+
>;
|
|
109
|
+
|
|
110
|
+
export interface R3FRootContextOptions {
|
|
62
111
|
/** World id — provenance for every debug-registry registration (matches the
|
|
63
112
|
* manifest root id by the same convention the classic adapter follows). */
|
|
64
113
|
readonly id: string;
|
|
@@ -72,18 +121,14 @@ export interface R3FWorldContextOptions {
|
|
|
72
121
|
readonly inputMapPath?: string | null | undefined;
|
|
73
122
|
}
|
|
74
123
|
|
|
75
|
-
/** What {@link
|
|
124
|
+
/** What {@link createR3FRootContext} returns — the engine runtime one R3F
|
|
76
125
|
* world owns, plus the wiring hooks `createR3FAdapter`'s `mount()` drives. */
|
|
77
|
-
export interface
|
|
78
|
-
/** The
|
|
79
|
-
*
|
|
80
|
-
readonly ctx:
|
|
126
|
+
export interface R3FRootRuntime {
|
|
127
|
+
/** The context this world's components read through `useGameContext()` —
|
|
128
|
+
* see {@link R3FGameContext} for exactly what an R3F world provides. */
|
|
129
|
+
readonly ctx: R3FGameContext;
|
|
81
130
|
/** The world-local phase runner `mount().update(dt)` ticks. */
|
|
82
131
|
readonly systems: ReturnType<typeof createSystemRunner>;
|
|
83
|
-
/** The engine's REAL ComponentManager for this world — `<Behavior>`
|
|
84
|
-
* attaches through it; expose it as `mounted.components` so
|
|
85
|
-
* `Game.queryByComponent` spans this world. */
|
|
86
|
-
readonly components: ComponentManager;
|
|
87
132
|
/** The mounted world's `SystemAdapters` bag (`mounted.systems`) —
|
|
88
133
|
* `ctx.registerSystemAdapter` writes into it; `debug` is pre-seeded. */
|
|
89
134
|
readonly systemAdapters: SystemAdapters;
|
|
@@ -101,68 +146,48 @@ export interface R3FWorldRuntime {
|
|
|
101
146
|
dispose(): void;
|
|
102
147
|
}
|
|
103
148
|
|
|
104
|
-
/** Install a throwing getter for a `GameContext` field this world genuinely
|
|
105
|
-
* does not build — a component reaching for it fails LOUDLY at its own call
|
|
106
|
-
* site, naming the limitation, instead of reading `undefined` and breaking
|
|
107
|
-
* somewhere downstream (or silently no-op-ing). Non-enumerable so spreads/
|
|
108
|
-
* serialization of the ctx never trip it. */
|
|
109
|
-
function defineAbsentCtxField(
|
|
110
|
-
target: Record<string, unknown>,
|
|
111
|
-
worldId: string,
|
|
112
|
-
key: string,
|
|
113
|
-
why: string,
|
|
114
|
-
): void {
|
|
115
|
-
Object.defineProperty(target, key, {
|
|
116
|
-
configurable: true,
|
|
117
|
-
enumerable: false,
|
|
118
|
-
get(): never {
|
|
119
|
-
throw new Error(
|
|
120
|
-
`R3F world "${worldId}": ctx.${key} is not available — ${why} ` +
|
|
121
|
-
'(An R3F world ctx provides: scene, camera, input, assets, systems, components, ' +
|
|
122
|
-
'debug, random, game, roots, registerSystemAdapter. For the full classic runtime, ' +
|
|
123
|
-
'use a .vscn scene root instead.)',
|
|
124
|
-
);
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
149
|
/**
|
|
130
|
-
* Build the engine runtime for one R3F world from a `
|
|
131
|
-
* wiring, at the same layer, as the classic `
|
|
150
|
+
* Build the engine runtime for one R3F world from a `ThreeHostContext` — the same
|
|
151
|
+
* wiring, at the same layer, as the classic `SetupThreeRootAdapter.mount()`
|
|
132
152
|
* performs for the subsystems an R3F world shares with it (debug, input,
|
|
133
|
-
* random
|
|
153
|
+
* random). The ones it does not build are not on
|
|
154
|
+
* {@link R3FGameContext} at all.
|
|
134
155
|
*/
|
|
135
|
-
export function
|
|
136
|
-
host:
|
|
137
|
-
options:
|
|
138
|
-
):
|
|
156
|
+
export function createR3FRootContext(
|
|
157
|
+
host: ThreeHostContext,
|
|
158
|
+
options: R3FRootContextOptions,
|
|
159
|
+
): R3FRootRuntime {
|
|
139
160
|
const { id, inputMapPath = DEFAULT_INPUT_MAP_PATH } = options;
|
|
140
161
|
|
|
141
162
|
// Same observer threading as the classic adapter — profiler sees this
|
|
142
163
|
// world's phases under the shared game profiler when hosted.
|
|
143
|
-
const systems = createSystemRunner(host.game?.profiler.systemObserver, '
|
|
164
|
+
const systems = createSystemRunner(host.game?.profiler.systemObserver, 'three');
|
|
144
165
|
|
|
145
166
|
// The mounted world's adapter surface (`mounted.systems`) — declared before
|
|
146
167
|
// ctx so `ctx.registerSystemAdapter` can close over it (classic parity: a
|
|
147
168
|
// game-owned capability like networking registers here from component code).
|
|
148
169
|
const systemAdapters: SystemAdapters = {};
|
|
149
170
|
|
|
171
|
+
// The world's live scene index — built in `setSceneCamera` (below), because
|
|
172
|
+
// fiber resolves the scene after this function returns. Same lifetime as
|
|
173
|
+
// `ctx.scene`, which is backfilled at the same moment.
|
|
174
|
+
let sceneIndex: SceneIndex | null = null;
|
|
175
|
+
|
|
150
176
|
const ctxRaw: Record<string, unknown> = {
|
|
151
177
|
systems,
|
|
152
178
|
assets: host.assets,
|
|
153
179
|
registerSystemAdapter: (kind: keyof SystemAdapters, adapter: SystemAdapters[typeof kind]) => {
|
|
154
180
|
// biome-ignore lint/suspicious/noExplicitAny: same per-key record write the classic adapter performs; correct by construction
|
|
155
181
|
(systemAdapters as any)[kind] = adapter;
|
|
182
|
+
host.game?.notifySystemAdaptersChanged();
|
|
156
183
|
},
|
|
157
184
|
};
|
|
158
|
-
const ctx = ctxRaw as unknown as
|
|
185
|
+
const ctx = ctxRaw as unknown as R3FGameContext;
|
|
159
186
|
if (host.game) {
|
|
160
187
|
ctx.game = host.game;
|
|
161
188
|
ctx.roots = host.game.roots;
|
|
189
|
+
ctx.playtest = host.game.playtest ?? null;
|
|
162
190
|
}
|
|
163
|
-
const components = createComponentManager(ctx);
|
|
164
|
-
ctxRaw['components'] = components;
|
|
165
|
-
|
|
166
191
|
// --- ctx.debug (gap 1) — the ONE registry per Game root, shared across
|
|
167
192
|
// every world mounted onto it; a bare mount with no Game shell gets a
|
|
168
193
|
// private, mount-local registry instead (same absence precedent as the
|
|
@@ -171,11 +196,24 @@ export function createR3FWorldContext(
|
|
|
171
196
|
(host.game ? getDebugRegistry(host.game) : null) ??
|
|
172
197
|
createDebugRegistry({ getTick: () => 0, getSimT: () => 0 });
|
|
173
198
|
systemAdapters.debug = debugRegistry.adapter;
|
|
174
|
-
ctx.debug = debugRegistry.
|
|
199
|
+
ctx.debug = debugRegistry.forRoot(id);
|
|
175
200
|
ctx.random =
|
|
176
201
|
(host.game ? getSeededRandom(host.game) : null) ??
|
|
177
202
|
createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED);
|
|
178
203
|
|
|
204
|
+
// --- ctx.clock (P3) — every world has a sim clock, R3F included. Same
|
|
205
|
+
// game-scoped reach-in as `ctx.random` above; a bare mount with no Game
|
|
206
|
+
// shell gets a private, mount-local clock that nothing flushes (there is no
|
|
207
|
+
// fixed loop to bind timers to). The mount-local fallback uses the SAME
|
|
208
|
+
// `disposeDebrisSubtree` the hosted path uses — it drops
|
|
209
|
+
// the subtree and disposes geometry/materials, with no Rapier owners
|
|
210
|
+
// because this ctx carries no `physics`/`rapierWorld` (the
|
|
211
|
+
// honest "this world owns no bodies").
|
|
212
|
+
const gameClock: SimClockInternal | null = host.game ? getSimClock(host.game) : null;
|
|
213
|
+
const clock: SimClockInternal =
|
|
214
|
+
gameClock ?? createSimClock({ dispose: (obj) => disposeDebrisSubtree(obj, []) });
|
|
215
|
+
ctx.clock = clock;
|
|
216
|
+
|
|
179
217
|
// --- ctx.input + the debug-registry input seams (gap 3) — the game-owned
|
|
180
218
|
// InputManager, polled by the game-scoped runner each frame; this world
|
|
181
219
|
// registers the SAME per-world seams the classic adapter registers so
|
|
@@ -228,63 +266,55 @@ export function createR3FWorldContext(
|
|
|
228
266
|
});
|
|
229
267
|
}
|
|
230
268
|
} else {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
269
|
+
// `input` IS a field of every R3F world's ctx — it is game-owned, and this
|
|
270
|
+
// particular mount has no Game to own it (a bare harness or a foreign
|
|
271
|
+
// host). A throwing getter names that at the read, instead of handing back
|
|
272
|
+
// `undefined` that breaks deep inside a frame callback. Non-enumerable so
|
|
273
|
+
// spreads/serialization of the ctx never trip it.
|
|
274
|
+
Object.defineProperty(ctxRaw, 'input', {
|
|
275
|
+
configurable: true,
|
|
276
|
+
enumerable: false,
|
|
277
|
+
get(): never {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`R3F world "${id}": ctx.input is game-owned and this mount has no Game host ` +
|
|
280
|
+
'(host.game is absent — bare harness/foreign host); mount through the vgai host ' +
|
|
281
|
+
'to get the shared InputManager.',
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
});
|
|
238
285
|
}
|
|
239
286
|
|
|
240
|
-
// --- Loud absence for the classic-only subsystems (gap 1's honesty rule:
|
|
241
|
-
// a thrown error naming the limitation beats a silent undefined). ---
|
|
242
|
-
const rapierWhy =
|
|
243
|
-
'R3F worlds do not build the first-party Rapier physics runtime; use a physics ' +
|
|
244
|
-
'solution inside the fiber tree (e.g. @react-three/rapier) or a classic .vscn world root.';
|
|
245
|
-
defineAbsentCtxField(ctxRaw, id, 'rapierWorld', rapierWhy);
|
|
246
|
-
defineAbsentCtxField(ctxRaw, id, 'rapier', rapierWhy);
|
|
247
|
-
defineAbsentCtxField(ctxRaw, id, 'physics', rapierWhy);
|
|
248
|
-
defineAbsentCtxField(ctxRaw, id, 'collisions', rapierWhy);
|
|
249
|
-
defineAbsentCtxField(
|
|
250
|
-
ctxRaw,
|
|
251
|
-
id,
|
|
252
|
-
'composer',
|
|
253
|
-
'R3F worlds render through react-three-fiber against the host renderer; there is no ' +
|
|
254
|
-
'postprocessing EffectComposer here (use fiber-native postprocessing inside the tree).',
|
|
255
|
-
);
|
|
256
|
-
defineAbsentCtxField(
|
|
257
|
-
ctxRaw,
|
|
258
|
-
id,
|
|
259
|
-
'audio',
|
|
260
|
-
'R3F worlds build no first-party audio context; use Web Audio directly (or drei audio ' +
|
|
261
|
-
'helpers) inside the tree.',
|
|
262
|
-
);
|
|
263
|
-
defineAbsentCtxField(
|
|
264
|
-
ctxRaw,
|
|
265
|
-
id,
|
|
266
|
-
'particles',
|
|
267
|
-
'R3F worlds build no three.quarks particle runtime; drive particles from the fiber tree.',
|
|
268
|
-
);
|
|
269
|
-
defineAbsentCtxField(
|
|
270
|
-
ctxRaw,
|
|
271
|
-
id,
|
|
272
|
-
'debugDraw',
|
|
273
|
-
'R3F worlds build no debug-draw helper; add helper objects to the fiber tree directly.',
|
|
274
|
-
);
|
|
275
|
-
|
|
276
287
|
return {
|
|
277
288
|
ctx,
|
|
278
289
|
systems,
|
|
279
|
-
components,
|
|
280
290
|
systemAdapters,
|
|
281
291
|
inputMapReady,
|
|
282
292
|
setSceneCamera(scene, camera): void {
|
|
293
|
+
const sameScene = ctxRaw['scene'] === scene && sceneIndex !== null;
|
|
283
294
|
ctxRaw['scene'] = scene;
|
|
284
295
|
ctxRaw['camera'] = camera;
|
|
296
|
+
// The live scene index (P2 `observe`) is a REAL value here, not a
|
|
297
|
+
// throwing getter — every three world has one. It can only be built once
|
|
298
|
+
// fiber has resolved the scene, which is the same moment `ctx.scene`
|
|
299
|
+
// itself becomes readable, so index and scene share one lifetime. A
|
|
300
|
+
// second commit (fiber handing back a different scene) replaces it.
|
|
301
|
+
//
|
|
302
|
+
// Keyed on the SCENE, so a camera-only backfill (a world swapping the
|
|
303
|
+
// default camera after mount) leaves the index — and every `observe`
|
|
304
|
+
// subscription hanging off it — alone.
|
|
305
|
+
if (sameScene) return;
|
|
306
|
+
sceneIndex?.dispose();
|
|
307
|
+
sceneIndex = createSceneIndex(scene);
|
|
308
|
+
ctxRaw['sceneIndex'] = sceneIndex;
|
|
285
309
|
},
|
|
286
310
|
dispose(): void {
|
|
287
|
-
|
|
311
|
+
// Same ownership rule as the classic adapter's teardown: dispose ONLY a
|
|
312
|
+
// clock this mount created. The game-scoped one belongs to
|
|
313
|
+
// `GameInternal.dispose()` — killing it here would freeze sim time and
|
|
314
|
+
// cancel every SIBLING world's timers.
|
|
315
|
+
if (!gameClock) clock.dispose();
|
|
316
|
+
sceneIndex?.dispose();
|
|
317
|
+
sceneIndex = null;
|
|
288
318
|
// Scoped strip when a Game hosts this world (sibling roots stay live);
|
|
289
319
|
// a bare standalone mount owns its private registry and clears it all.
|
|
290
320
|
if (host.game) debugRegistry.strip(id);
|