@vgai/engine 0.5.2 → 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
|
@@ -1,71 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `createR3FAdapter` — wraps a react-three-fiber tree as a first-party
|
|
3
|
-
* `
|
|
4
|
-
* world: the engine hands `mount()` a `
|
|
3
|
+
* `RootAdapter` so an R3F scene mounts as an ordinary `kind: "three"`
|
|
4
|
+
* world: the engine hands `mount()` a `ThreeHostContext` (ITS OWN renderer +
|
|
5
5
|
* canvas + gated loop, per `../adapter/host-context.ts`); this returns a
|
|
6
|
-
* `
|
|
6
|
+
* `MountedThreeRoot` (`../adapter/root-adapter.ts`) whose `scene`/`camera`
|
|
7
7
|
* are fiber's REAL `THREE.Scene`/`THREE.Camera` instances.
|
|
8
8
|
*
|
|
9
|
-
* Upstreamed from `examples/r3f-first-party/src/r3f-adapter.tsx` (R4
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `components` registry threaded into `EngineBridge` so `<Behavior>` can
|
|
15
|
-
* resolve project component classes without the engine importing project
|
|
16
|
-
* code. External adopters (alien-stories-style) keep the exact same calling
|
|
17
|
-
* contract.
|
|
9
|
+
* Upstreamed from `examples/r3f-first-party/src/r3f-adapter.tsx` (R4 — the
|
|
10
|
+
* `@engine/world3d-react` opt-in module, mirroring how `pixi/` is the
|
|
11
|
+
* PixiJS surface's opt-in home). The mount semantics
|
|
12
|
+
* are byte-for-byte the example's live-proven Phase-1 bridge. External adopters
|
|
13
|
+
* (alien-stories-style) keep the exact same calling contract.
|
|
18
14
|
*
|
|
19
|
-
* three.js identity
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* `examples/r3f-first-party/vite.config.ts`).
|
|
15
|
+
* three.js identity: this module never imports `three` itself for scene objects
|
|
16
|
+
* — fiber's internal `import * as THREE from 'three'` must resolve to the SAME
|
|
17
|
+
* instance the engine's `ThreeHostContext.three` points at, which the importing
|
|
18
|
+
* project guarantees via its Vite `resolve.dedupe: ['three', 'react',
|
|
19
|
+
* 'react-dom']` (see `examples/r3f-first-party/vite.config.ts`).
|
|
25
20
|
*/
|
|
26
21
|
|
|
27
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
advance,
|
|
24
|
+
createRoot,
|
|
25
|
+
extend,
|
|
26
|
+
events as pointerEvents,
|
|
27
|
+
type RootState,
|
|
28
|
+
} from '@react-three/fiber';
|
|
28
29
|
import { createElement, type ReactNode } from 'react';
|
|
29
|
-
import type {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
30
|
+
import type { MountedThreeRoot, RootAdapter, ThreeHostContext } from '../adapter';
|
|
31
|
+
import { WorldProvider } from '../react/world-state';
|
|
32
|
+
import { EngineBridge, type EngineBridgeValue } from './engine-bridge';
|
|
33
|
+
import { applyWorldRendererConfig, type WorldRendererConfig } from './renderer-config';
|
|
34
|
+
import { createR3FRootContext } from './world-context';
|
|
32
35
|
|
|
33
|
-
/** What {@link createR3FAdapter} needs to build one `
|
|
36
|
+
/** What {@link createR3FAdapter} needs to build one `RootAdapter`. */
|
|
34
37
|
export interface CreateR3FAdapterOptions {
|
|
35
|
-
/** Stable id (telemetry/registry/conformance) — `
|
|
38
|
+
/** Stable id (telemetry/registry/conformance) — `RootAdapter.id`. */
|
|
36
39
|
readonly id: string;
|
|
37
40
|
/** The R3F scene tree to mount — drei helpers, `useFrame` hooks, etc. all
|
|
38
41
|
* work unchanged (design §1.C: "drei works unchanged (it is fiber-context
|
|
39
42
|
* userland)"). */
|
|
40
43
|
readonly content: ReactNode;
|
|
41
|
-
/** The project's behavior registry — `<Behavior type="…">` resolves against
|
|
42
|
-
* it through `EngineBridge`. Optional: a tree with no `<Behavior>` usage
|
|
43
|
-
* needs none; a `<Behavior>` rendered without it degrades loudly (warn,
|
|
44
|
-
* no attach) rather than crashing. */
|
|
45
|
-
readonly components?: BehaviorRegistry | undefined;
|
|
46
44
|
/** Input map loaded through the game-owned `Game.loadInputMap` (load-once
|
|
47
45
|
* across roots). Defaults to the classic adapter's conventional path,
|
|
48
46
|
* `/inputmaps/default.inputmap.json`; `null` opts out (a world with no
|
|
49
47
|
* actions). A missing/invalid map degrades loudly without failing the
|
|
50
48
|
* mount — see `world-context.ts`. */
|
|
51
49
|
readonly inputMapPath?: string | null | undefined;
|
|
50
|
+
/** The colour pipeline this world was AUTHORED for, applied to the host's renderer for the life
|
|
51
|
+
* of the mount and restored on dispose. Omit it (every world here does) to keep the host's own
|
|
52
|
+
* defaults; declare it when the world's colours were picked against a different engine's
|
|
53
|
+
* pipeline — see `./renderer-config.ts`. */
|
|
54
|
+
readonly renderer?: WorldRendererConfig | undefined;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
/**
|
|
55
|
-
* Build a `
|
|
58
|
+
* Build a `RootAdapter` that mounts `options.content` through
|
|
56
59
|
* react-three-fiber, gated entirely by the host's own loop and rendering
|
|
57
60
|
* through the host's own `WebGLRenderer` — never a second renderer, never a
|
|
58
61
|
* second `requestAnimationFrame` loop (design §1.C).
|
|
59
62
|
*/
|
|
60
|
-
export function createR3FAdapter(options: CreateR3FAdapterOptions):
|
|
61
|
-
const { id, content,
|
|
63
|
+
export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter {
|
|
64
|
+
const { id, content, inputMapPath, renderer: rendererConfig } = options;
|
|
62
65
|
|
|
63
66
|
return {
|
|
64
67
|
id,
|
|
65
68
|
|
|
66
|
-
async mount(host:
|
|
67
|
-
// Headless honesty
|
|
68
|
-
//
|
|
69
|
+
async mount(host: ThreeHostContext): Promise<MountedThreeRoot> {
|
|
70
|
+
// Headless honesty — every `mount()` must work headless: the
|
|
71
|
+
// conformance kit / CI
|
|
69
72
|
// runs `mount()` in Node — no canvas, no WebGL, no fiber reconciler.
|
|
70
73
|
// Guard the ENTIRE fiber mount behind `!host.headless` and return a
|
|
71
74
|
// bare, real scene + camera instead (still `instanceof
|
|
@@ -78,7 +81,7 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): GameAdapter
|
|
|
78
81
|
const scene = new host.three.Scene();
|
|
79
82
|
const camera = new host.three.PerspectiveCamera();
|
|
80
83
|
return {
|
|
81
|
-
kind: '
|
|
84
|
+
kind: 'three',
|
|
82
85
|
scene,
|
|
83
86
|
camera,
|
|
84
87
|
drivesOwnLoop: false,
|
|
@@ -99,31 +102,37 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): GameAdapter
|
|
|
99
102
|
// per mount is idempotent.
|
|
100
103
|
extend(host.three as unknown as Parameters<typeof extend>[0]);
|
|
101
104
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// the
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
// components tick in engine phase order and freeze under pause exactly
|
|
108
|
-
// like `useFrame` work does.
|
|
105
|
+
// The engine runtime this world OWNS: a real SystemRunner,
|
|
106
|
+
// provided to the R3F tree via `EngineBridge`.
|
|
107
|
+
// `update(dt)` below runs the phases under the host loop, so registered
|
|
108
|
+
// systems tick in engine phase order and freeze under
|
|
109
|
+
// pause exactly like `useFrame` work does.
|
|
109
110
|
//
|
|
110
|
-
// HONESTY NOTE: the manager's ctx is
|
|
111
|
-
//
|
|
112
|
-
// (`./world-context.ts`):
|
|
113
|
-
// and `ctx.input` (the
|
|
114
|
-
// virtual-input/actions/trace
|
|
115
|
-
// input map loaded). The
|
|
116
|
-
// build (Rapier, composer,
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
const {
|
|
122
|
-
const
|
|
123
|
-
//
|
|
124
|
-
//
|
|
111
|
+
// HONESTY NOTE: the manager's ctx is an `R3FGameContext`, not the full
|
|
112
|
+
// `GameContext`. The shared game-scoped subsystems are REAL
|
|
113
|
+
// classic-adapter parity (`./world-context.ts`):
|
|
114
|
+
// `ctx.debug`/`ctx.random`/`ctx.game`/`ctx.roots`, and `ctx.input` (the
|
|
115
|
+
// game-owned InputManager, with this world's virtual-input/actions/trace
|
|
116
|
+
// debug seams registered and the project's input map loaded). The
|
|
117
|
+
// subsystems an R3F world genuinely does not build (Rapier, composer,
|
|
118
|
+
// audio, particles, debugDraw) are absent from that TYPE, so reaching
|
|
119
|
+
// for one is a compile error rather than anything that can happen at
|
|
120
|
+
// runtime. `scene`/`camera` are backfilled right after fiber's first
|
|
121
|
+
// commit resolves them, before any tick can run.
|
|
122
|
+
const runtime = createR3FRootContext(host, { id, inputMapPath });
|
|
123
|
+
const { systems } = runtime;
|
|
124
|
+
// `ctx` rides the bridge so ordinary components can reach engine
|
|
125
|
+
// services through `useGameContext()`.
|
|
126
|
+
const bridge: EngineBridgeValue = { ctx: runtime.ctx };
|
|
127
|
+
// Actions must exist before any component's init()/first frame reads
|
|
128
|
+
// them — wait for the (never-rejecting) input-map load before the first
|
|
129
|
+
// commit.
|
|
125
130
|
await runtime.inputMapReady;
|
|
126
131
|
|
|
132
|
+
// The world's own colour pipeline. Applied AFTER `root.configure()` below — see the comment
|
|
133
|
+
// at its call site for why "before" is the version that silently does nothing.
|
|
134
|
+
let restoreRendererConfig: () => void = () => {};
|
|
135
|
+
|
|
127
136
|
const canvas = host.surface.canvas;
|
|
128
137
|
const root = createRoot(canvas);
|
|
129
138
|
|
|
@@ -149,6 +158,28 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): GameAdapter
|
|
|
149
158
|
// renders THROUGH `host.renderer` instead of constructing its own
|
|
150
159
|
// `WebGLRenderer`.
|
|
151
160
|
gl: host.renderer,
|
|
161
|
+
// …and the host's shadow map, not fiber's.
|
|
162
|
+
//
|
|
163
|
+
// `configure()` runs `gl.shadowMap.enabled = !!shadows`
|
|
164
|
+
// UNCONDITIONALLY, against its own default of `false`. Passing nothing
|
|
165
|
+
// therefore does not mean "leave it alone" — it means "turn shadows
|
|
166
|
+
// off on the renderer the host just configured". Every three root
|
|
167
|
+
// silently did that (FX-5): `applyRendererSettings` sets
|
|
168
|
+
// `shadowMap.enabled` from the `shadows` render feature, which
|
|
169
|
+
// defaults ON, and the setting had no surviving reader on this path —
|
|
170
|
+
// so `castShadow`/`receiveShadow` all over a world produced nothing
|
|
171
|
+
// and the scene read flat, with no error anywhere.
|
|
172
|
+
//
|
|
173
|
+
// Reading it back off the renderer is exactly "don't clobber": a
|
|
174
|
+
// boolean also pins `shadowMap.type` to `PCFSoftShadowMap`, which is
|
|
175
|
+
// what `createHostRenderer` already sets, so the round trip is
|
|
176
|
+
// lossless and the HOST stays the one deciding.
|
|
177
|
+
//
|
|
178
|
+
// Optional-chained even though `WebGLRenderer.shadowMap` is not
|
|
179
|
+
// optional: a headless/stub host renderer is a real shape in this
|
|
180
|
+
// repo's tests, and fiber's own `if (gl.shadowMap)` guard means the
|
|
181
|
+
// value it reads there is ignored anyway.
|
|
182
|
+
shadows: host.renderer.shadowMap?.enabled ?? false,
|
|
152
183
|
// The engine's gated loop is the ONLY loop — fiber must never run
|
|
153
184
|
// its own rAF (that would defeat editor pause; design §1.C's
|
|
154
185
|
// `drivesOwnLoop: false` contract, guide §2's loop-model note).
|
|
@@ -159,20 +190,70 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): GameAdapter
|
|
|
159
190
|
// to 0 (this bridge always fills its whole canvas; no offset
|
|
160
191
|
// viewport in v1).
|
|
161
192
|
size: { width: host.surface.width, height: host.surface.height, top: 0, left: 0 },
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
193
|
+
// Fiber's pointer-event manager, passed EXPLICITLY.
|
|
194
|
+
//
|
|
195
|
+
// This comment used to say `events` was "deliberately left at fiber's
|
|
196
|
+
// default (binds to `gl.domElement`)". That was false, and it cost
|
|
197
|
+
// every R3F world in this engine its mesh-level pointer props —
|
|
198
|
+
// `onClick`, `onPointerOver`, `onPointerMissed` were dead everywhere.
|
|
199
|
+
// There IS no default for a bare `createRoot`: the
|
|
200
|
+
// `events = createPointerEvents` default lives in the `<Canvas>`
|
|
201
|
+
// COMPONENT, which this bridge does not use, and `configure` gates on
|
|
202
|
+
// `if (events && !state.events.handlers)`. With `events` undefined the
|
|
203
|
+
// store kept its initial `{ priority, enabled, connected: false }` —
|
|
204
|
+
// no handlers, nothing bound to the canvas, no error.
|
|
205
|
+
//
|
|
206
|
+
// Found by the WO-P6 builder, whose control experiment isolated the
|
|
207
|
+
// variable properly: the same stub renderer, the same
|
|
208
|
+
// `frameloop: 'never'`, the same jsdom canvas and pointer sequence,
|
|
209
|
+
// differing ONLY in passing `events` — and `onClick` fired. Fiber is
|
|
210
|
+
// fine under a host-driven loop; we omitted one property.
|
|
211
|
+
//
|
|
212
|
+
// (Design O1's open question about a delegating multi-canvas input
|
|
213
|
+
// router still only matters for a shell+net composite; this bridge is
|
|
214
|
+
// the single-canvas world O1 says is unaffected.)
|
|
215
|
+
events: pointerEvents,
|
|
167
216
|
onCreated: (state) => resolveState(state),
|
|
168
217
|
});
|
|
218
|
+
|
|
219
|
+
// THE WORLD'S OWN COLOUR PIPELINE — and it must land HERE, after `configure()`.
|
|
220
|
+
//
|
|
221
|
+
// `configure()` writes `outputColorSpace` and `toneMapping` onto the renderer
|
|
222
|
+
// UNCONDITIONALLY, from its own `linear`/`flat` props whose defaults are `false`/`false` —
|
|
223
|
+
// i.e. sRGB output and ACES. That is the SAME trap as the `shadows` note above, and it is
|
|
224
|
+
// silent: applying a world's config before `configure()` leaves every field set and every
|
|
225
|
+
// pixel unchanged (measured on the squash-the-creeps Godot port — the ground stayed at byte
|
|
226
|
+
// 138 with `toneMapping: 'none'` declared and applied).
|
|
227
|
+
//
|
|
228
|
+
// Applying it here is still before any frame: `advance()` only runs from `update()` below,
|
|
229
|
+
// and `render()` on the next line merely reconciles.
|
|
230
|
+
if (rendererConfig !== undefined) {
|
|
231
|
+
restoreRendererConfig = applyWorldRendererConfig(host.three, host.renderer, rendererConfig);
|
|
232
|
+
}
|
|
169
233
|
// No `<StrictMode>` (design §1.C: "no StrictMode: host mounts once")
|
|
170
234
|
// — this bridge mounts exactly once per `mount()` call; StrictMode's
|
|
171
235
|
// deliberate double-invoke of effects would double-subscribe
|
|
172
236
|
// `useFrame` callbacks against a host loop that only ticks once per
|
|
173
237
|
// frame. The `EngineBridge` provider threads the component runtime
|
|
174
|
-
// (above)
|
|
175
|
-
|
|
238
|
+
// (above) into the tree (W3), where `useGameContext()` reads it.
|
|
239
|
+
// R3F IS React, so the React seam every dom root already has —
|
|
240
|
+
// `useWorldState`, `useDebugProvider`/`useDebugCommand`/`useDebugEmit`
|
|
241
|
+
// (`../react/world-state`) — should work identically inside a three root.
|
|
242
|
+
// Without this provider a behavior-as-a-hook could only reach the debug
|
|
243
|
+
// registry by destructuring `useGameContext()` and calling
|
|
244
|
+
// `debug?.registerStateProvider(...)` by hand, which has no unmount
|
|
245
|
+
// story: the template's follow-camera hook leaked a provider per remount
|
|
246
|
+
// until this landed. `host.game` is genuinely absent in bare/headless
|
|
247
|
+
// hosts, so the provider is conditional and those hooks stay the inert
|
|
248
|
+
// no-ops they already document themselves to be.
|
|
249
|
+
const game = runtime.ctx.game;
|
|
250
|
+
root.render(
|
|
251
|
+
createElement(
|
|
252
|
+
EngineBridge.Provider,
|
|
253
|
+
{ value: bridge },
|
|
254
|
+
game ? createElement(WorldProvider, { game }, content) : content,
|
|
255
|
+
),
|
|
256
|
+
);
|
|
176
257
|
// A reconcile-time crash (e.g. a missing `extend` catalogue entry)
|
|
177
258
|
// surfaces as an uncaught window error and `onCreated` never fires —
|
|
178
259
|
// without this guard, `mount()` would await `statePromise` FOREVER and
|
|
@@ -183,6 +264,7 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): GameAdapter
|
|
|
183
264
|
cleanup();
|
|
184
265
|
root.unmount();
|
|
185
266
|
runtime.dispose();
|
|
267
|
+
restoreRendererConfig();
|
|
186
268
|
reject(
|
|
187
269
|
new Error(
|
|
188
270
|
`r3f-adapter: fiber crashed before its first commit — ${event.message} ` +
|
|
@@ -194,6 +276,7 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): GameAdapter
|
|
|
194
276
|
cleanup();
|
|
195
277
|
root.unmount();
|
|
196
278
|
runtime.dispose();
|
|
279
|
+
restoreRendererConfig();
|
|
197
280
|
reject(
|
|
198
281
|
new Error(
|
|
199
282
|
'r3f-adapter: onCreated did not fire within 10s — the R3F tree never reached ' +
|
|
@@ -228,26 +311,43 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): GameAdapter
|
|
|
228
311
|
// `update(dt)` tick, so no component ever observes them missing.
|
|
229
312
|
runtime.setSceneCamera(state.scene, state.camera);
|
|
230
313
|
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
// the
|
|
237
|
-
|
|
314
|
+
// THE STORE IS THE TRUTH, NOT THE `onCreated` SNAPSHOT.
|
|
315
|
+
//
|
|
316
|
+
// `state` is one immutable zustand snapshot: fiber's store is
|
|
317
|
+
// `create<RootState>()`, and every `set(...)` produces a NEW state
|
|
318
|
+
// object rather than mutating this one. So a world that legitimately
|
|
319
|
+
// REPLACES the default camera — `useThree(s => s.set)({ camera })`,
|
|
320
|
+
// which is exactly what drei's `makeDefault` does — never reaches
|
|
321
|
+
// anything read off `state` directly.
|
|
322
|
+
//
|
|
323
|
+
// That is not cosmetic: fiber's own `update()` renders
|
|
324
|
+
// `state.gl.render(state.scene, state.camera)` off the state object it
|
|
325
|
+
// is HANDED, so `advance(t, true, state)` below pinned the render
|
|
326
|
+
// camera to fiber's default `PerspectiveCamera` at (0, 0, 5) forever.
|
|
327
|
+
// Measured on the squash-the-creeps Godot port (WO-G14): the world set
|
|
328
|
+
// its authored `OrthographicCamera` and the frame still rendered from
|
|
329
|
+
// (0, 0, 5) — a point INSIDE that game's 60x2x60 ground box, so every
|
|
330
|
+
// ground polygon was back-facing, was culled, and the "ground" was the
|
|
331
|
+
// cleared canvas. Nothing anywhere reported an error; edit mode looked
|
|
332
|
+
// right because the editor renders with its own camera.
|
|
333
|
+
//
|
|
334
|
+
// `state.get()` is fiber's own `StoreApi.getState`, carried on
|
|
335
|
+
// `RootState` for precisely this reason, so reading it per frame is the
|
|
336
|
+
// supported way to see the live root. It is a plain map read.
|
|
337
|
+
const live = (): RootState => state.get();
|
|
238
338
|
|
|
239
339
|
// The engine drives every `useFrame` through the mounted world's
|
|
240
|
-
// `update(dt)` hook
|
|
340
|
+
// `update(dt)` hook, never off a raw host-loop callback. That is the
|
|
241
341
|
// whole pause story: `runFrameImpl` (`runtime/game.ts`) calls
|
|
242
342
|
// `mounted.update?.(dt)` per substep for a host-driven
|
|
243
343
|
// (`drivesOwnLoop: false`) world and SKIPS it while that world is
|
|
244
|
-
// frozen, and `Game.play.step()` ticks it exactly once — whereas
|
|
245
|
-
//
|
|
246
|
-
// not
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
344
|
+
// frozen, and `Game.play.step()` ticks it exactly once — whereas any
|
|
345
|
+
// callback run outside `game.runFrame`'s gate would fire
|
|
346
|
+
// unconditionally, pause or not. Advancing fiber from such a callback
|
|
347
|
+
// LOOKS right and silently breaks acceptance gate 2 — proven by the
|
|
348
|
+
// 36-r3f-first-party e2e, whose paused instance-matrix samples kept
|
|
349
|
+
// moving until this moved to `update`. (The former `ThreeHostContext.loop`
|
|
350
|
+
// seam that made the wrong path reachable was deleted, zero consumers.)
|
|
251
351
|
//
|
|
252
352
|
// `advance(timestamp, runGlobalEffects, state)`'s `timestamp` is
|
|
253
353
|
// consumed as `THREE.Clock.elapsedTime` DIRECTLY when
|
|
@@ -263,9 +363,11 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): GameAdapter
|
|
|
263
363
|
// resumed frame as one giant `useFrame` delta — the exact "time passed
|
|
264
364
|
// while frozen" illusion acceptance gate 2 forbids.
|
|
265
365
|
let elapsed = 0;
|
|
366
|
+
/** The camera `ctx` was last backfilled with — see `update()`. */
|
|
367
|
+
let backfilledCamera = state.camera;
|
|
266
368
|
|
|
267
369
|
return {
|
|
268
|
-
kind: '
|
|
370
|
+
kind: 'three',
|
|
269
371
|
// Fiber's REAL `THREE.Scene`/`THREE.Camera`. Checkout development
|
|
270
372
|
// dedupes `three`; a registry-installed project and the prebundled
|
|
271
373
|
// editor can legitimately have distinct constructor identities, so
|
|
@@ -274,36 +376,49 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): GameAdapter
|
|
|
274
376
|
// `state.camera`'s type (`Camera = (OrthographicCamera |
|
|
275
377
|
// PerspectiveCamera) & { manual?: boolean }`) is a structural subtype
|
|
276
378
|
// of `THREE.Camera`, so no cast is needed.
|
|
277
|
-
|
|
278
|
-
|
|
379
|
+
//
|
|
380
|
+
// Both are GETTERS over the live store, not snapshot fields: the host
|
|
381
|
+
// and the editor must see the camera the world actually renders
|
|
382
|
+
// through, including one the world swapped in after the first commit
|
|
383
|
+
// (see `live` above).
|
|
384
|
+
get scene() {
|
|
385
|
+
return live().scene;
|
|
386
|
+
},
|
|
387
|
+
get camera() {
|
|
388
|
+
return live().camera;
|
|
389
|
+
},
|
|
279
390
|
drivesOwnLoop: false,
|
|
280
|
-
// The engine's REAL ComponentManager for this world — what makes
|
|
281
|
-
// `Game.queryByComponent` see `<Behavior>`-attached components
|
|
282
|
-
// (`runtime/game.ts` aggregates this optional capability for every
|
|
283
|
-
// non-first-party mount that exposes one).
|
|
284
|
-
components,
|
|
285
391
|
// Adapter surface: `debug` pre-seeded (the shared game registry's
|
|
286
392
|
// adapter); game code adds capabilities via
|
|
287
393
|
// `ctx.registerSystemAdapter` exactly as in a classic world.
|
|
288
394
|
systems: runtime.systemAdapters,
|
|
289
395
|
update(dt: number): void {
|
|
290
396
|
elapsed += dt;
|
|
291
|
-
|
|
397
|
+
const current = live();
|
|
398
|
+
// A world that swapped the default camera gets it onto `ctx` too —
|
|
399
|
+
// `ctx.camera` and the rendered camera are the same fact, and a
|
|
400
|
+
// component reading one while the frame uses the other is the
|
|
401
|
+
// silent kind of wrong. Identity-gated, so this is one comparison
|
|
402
|
+
// per frame in the overwhelmingly common no-swap case.
|
|
403
|
+
if (current.camera !== backfilledCamera) {
|
|
404
|
+
backfilledCamera = current.camera;
|
|
405
|
+
runtime.setSceneCamera(current.scene, current.camera);
|
|
406
|
+
}
|
|
407
|
+
// Engine phases FIRST (registered systems mutate transforms),
|
|
292
408
|
// then fiber's advance (useFrame callbacks + the actual render see
|
|
293
|
-
// the fresh state). Both
|
|
294
|
-
// `<Behavior>` — are host-gated: neither runs while frozen.
|
|
409
|
+
// the fresh state). Both are host-gated: neither runs while frozen.
|
|
295
410
|
systems.run(dt);
|
|
296
|
-
advance(elapsed, true,
|
|
411
|
+
advance(elapsed, true, current);
|
|
297
412
|
},
|
|
298
413
|
resize(width: number, height: number): void {
|
|
299
|
-
|
|
414
|
+
live().setSize(width, height);
|
|
300
415
|
},
|
|
301
416
|
dispose(): void {
|
|
302
|
-
// Unmount FIRST so
|
|
303
|
-
//
|
|
304
|
-
// scoped debug-registry strip).
|
|
417
|
+
// Unmount FIRST so component effect cleanups run, then tear the
|
|
418
|
+
// runtime down (scoped debug-registry strip).
|
|
305
419
|
root.unmount();
|
|
306
420
|
runtime.dispose();
|
|
421
|
+
restoreRendererConfig();
|
|
307
422
|
},
|
|
308
423
|
};
|
|
309
424
|
},
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `r3fRootFactory` — mount a `three` root whose entry module DEFAULT-EXPORTS a
|
|
3
|
+
* React component.
|
|
4
|
+
*
|
|
5
|
+
* The idiomatic-shape fix (D26). A model asked for an R3F world writes:
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* export default function World() {
|
|
9
|
+
* return <><ambientLight /><mesh>…</mesh></>;
|
|
10
|
+
* }
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* It does NOT write `export const adapter = createR3FAdapter({ id, content })`.
|
|
14
|
+
* That export is pure vgai ceremony wrapped around a component the author
|
|
15
|
+
* already wrote, and it is the second-largest non-idiomatic thing left in a
|
|
16
|
+
* world file (the largest is the missing `<Canvas>`, which is a separate
|
|
17
|
+
* question about who owns the canvas — see D26).
|
|
18
|
+
*
|
|
19
|
+
* The `dom` root already settled this convention: its factory reads
|
|
20
|
+
* `entryModule.default` and wraps it. This is the same contract for three
|
|
21
|
+
* roots, so both surfaces answer "what do I export?" the same way.
|
|
22
|
+
*
|
|
23
|
+
* ## Why this is NOT in `mount-game.ts`
|
|
24
|
+
*
|
|
25
|
+
* `defaultThreeAdapterFactory` lives beside the registry, and the registry
|
|
26
|
+
* module is deliberately dependency-free: its own doc notes threejs is safe to
|
|
27
|
+
* ship there because `three` is already an unconditional engine dependency.
|
|
28
|
+
* `@react-three/fiber` is NOT — it is the PROJECT's dependency, and
|
|
29
|
+
* `world3d-react` is the opt-in module that owns it. Putting this factory
|
|
30
|
+
* there would make R3F unconditional for every game, including ones with no
|
|
31
|
+
* React at all.
|
|
32
|
+
*
|
|
33
|
+
* ## Compatibility
|
|
34
|
+
*
|
|
35
|
+
* An `adapter` export still wins, so existing worlds are untouched and a game
|
|
36
|
+
* needing full control over its own `RootAdapter` keeps it.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { type ComponentType, createElement } from 'react';
|
|
40
|
+
import type { RootAdapter } from '../adapter/root-adapter';
|
|
41
|
+
import type { AdapterSurfaceFactory } from '../runtime/mount-game';
|
|
42
|
+
import { createR3FAdapter } from './r3f-adapter';
|
|
43
|
+
|
|
44
|
+
interface R3FEntryModuleExports {
|
|
45
|
+
/** The idiomatic shape: the world IS a component. */
|
|
46
|
+
readonly default?: ComponentType;
|
|
47
|
+
/** Full control, and still the winner when present. */
|
|
48
|
+
readonly adapter?: RootAdapter;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What a three entry module MEANS, in one place.
|
|
53
|
+
*
|
|
54
|
+
* Two callers need this answer and must never disagree about it: the runtime
|
|
55
|
+
* mount ({@link r3fRootFactory}, below) and the EDITOR's design session
|
|
56
|
+
* (`packages/editor/src/authoring/r3f-design-session.ts`), which design-mounts
|
|
57
|
+
* the same entry so edit mode authors the live fiber scene. When only the
|
|
58
|
+
* runtime knew about the default-export shape, the converted starter played
|
|
59
|
+
* fine under `npm run game` and edit mode showed "(no scene loaded)".
|
|
60
|
+
*
|
|
61
|
+
* Returns `null` when the module is neither shape — the callers differ on what
|
|
62
|
+
* to do about that (the factory throws, the design session declines the world
|
|
63
|
+
* and leaves existing paths alone).
|
|
64
|
+
*/
|
|
65
|
+
export function resolveR3FEntryAdapter(entryModule: unknown, rootId: string): RootAdapter | null {
|
|
66
|
+
const mod = entryModule as R3FEntryModuleExports | undefined;
|
|
67
|
+
if (mod?.adapter) return mod.adapter;
|
|
68
|
+
if (typeof mod?.default === 'function') {
|
|
69
|
+
return createR3FAdapter({
|
|
70
|
+
id: rootId,
|
|
71
|
+
content: createElement(mod.default),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Register with `registerAdapter('three', r3fRootFactory)`.
|
|
79
|
+
*
|
|
80
|
+
* Resolution order mirrors `defaultThreeAdapterFactory`'s (explicit adapter
|
|
81
|
+
* wins) and then accepts the default-exported component.
|
|
82
|
+
*/
|
|
83
|
+
export const r3fRootFactory: AdapterSurfaceFactory = (root, ctx) => {
|
|
84
|
+
const adapter = resolveR3FEntryAdapter(ctx.entryModule, root.id);
|
|
85
|
+
if (adapter) return { kind: 'three', adapter };
|
|
86
|
+
throw new Error(
|
|
87
|
+
`r3fRootFactory: entry module "${root.entry ?? '(none)'}" for root "${root.id}" must ` +
|
|
88
|
+
'default-export a React component (`export default function World() { … }`) — or export ' +
|
|
89
|
+
'an `adapter` for full control.',
|
|
90
|
+
);
|
|
91
|
+
};
|