@vgai/engine 0.5.21 → 0.5.22
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/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 +45 -3
- package/dist/adapter/native-debug-module.d.ts.map +1 -1
- package/dist/adapter/native-debug-module.js +128 -10
- package/dist/adapter/renderer-config.d.ts +1 -1
- package/dist/adapter/renderer-config.js +1 -1
- 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/sim-clock.d.ts +14 -2
- package/dist/core/sim-clock.d.ts.map +1 -1
- package/dist/core/sim-clock.js +14 -2
- 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/input/input-manager.d.ts +15 -0
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +15 -0
- 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/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 +15 -0
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +40 -8
- package/dist/runtime/mount-game.d.ts.map +1 -1
- package/dist/runtime/mount-game.js +18 -2
- package/dist/runtime/mount-manifest.d.ts.map +1 -1
- package/dist/runtime/mount-manifest.js +4 -2
- package/dist/runtime/types.d.ts +1 -1
- 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 +19 -25
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +396 -50
- package/dist/world3d-react/rapier-physics-bridge.d.ts +24 -12
- package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
- package/dist/world3d-react/rapier-physics-bridge.js +82 -49
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +3 -3
- package/src/adapter/manifest-interpreter.ts +64 -0
- package/src/adapter/native-debug-module.ts +212 -15
- package/src/adapter/renderer-config.ts +1 -1
- 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/sim-clock.ts +14 -2
- package/src/dev/instruments.ts +108 -11
- package/src/input/input-manager.ts +15 -0
- 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/runtime/game-input-seams.ts +108 -0
- package/src/runtime/game.ts +65 -7
- package/src/runtime/mount-game.ts +23 -1
- package/src/runtime/mount-manifest.ts +4 -2
- package/src/runtime/types.ts +1 -1
- package/src/world3d-react/index.ts +15 -31
- package/src/world3d-react/r3f-root-factory.tsx +444 -53
- package/src/world3d-react/rapier-physics-bridge.tsx +88 -49
- 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/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
|
@@ -40,18 +40,20 @@
|
|
|
40
40
|
* is how a `SystemAdapters` member says "not supported".
|
|
41
41
|
*
|
|
42
42
|
* ── RESOURCE OWNERSHIP, STATED ONCE ─────────────────────────────────────────
|
|
43
|
-
* OWNER: this component owns
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* a world that unmounts `<Physics>` leaves no stale adapter
|
|
54
|
-
*
|
|
43
|
+
* OWNER: this component owns one physics ADAPTER, published onto the ONE
|
|
44
|
+
* module-scoped slot `rapierPhysicsSystem()` forwards to (GAME-GLOBAL — the
|
|
45
|
+
* entry's static `systems.physics` declaration cannot be per-root). It allocates nothing
|
|
46
|
+
* else — no timers, no subscriptions, no per-frame work (see the phase note
|
|
47
|
+
* below), and no Rapier objects: the bodies it edits belong to `<Physics>`.
|
|
48
|
+
* SHARERS: every reader of `game.systemAdapters.physics` — the colliders
|
|
49
|
+
* instrument, the editor's transform coordination. They read; they never
|
|
50
|
+
* dispose.
|
|
51
|
+
* TEARDOWN: the effect's cleanup, and nothing else — it clears the SAME key
|
|
52
|
+
* and the slot it set (identity-guarded), so a play stop/start, a hot
|
|
53
|
+
* restart, or a world that unmounts `<Physics>` leaves no stale adapter
|
|
54
|
+
* behind. Because the slot is game-global, mount exactly ONE bridge per
|
|
55
|
+
* GAME: with two, the later mount wins the slot and the later unmount clears
|
|
56
|
+
* it while the earlier bridge is still live.
|
|
55
57
|
*
|
|
56
58
|
* PHASE ORDERING: none to respect — this component registers pull-only
|
|
57
59
|
* callbacks and runs no per-frame work. Every method it exposes is invoked
|
|
@@ -62,7 +64,6 @@ import { useRapier } from '@react-three/rapier';
|
|
|
62
64
|
import { useEffect } from 'react';
|
|
63
65
|
import * as THREE from 'three';
|
|
64
66
|
import { createRapierBodyEditing, } from '../adapter/rapier-physics-adapter';
|
|
65
|
-
import { useGameContext } from './engine-bridge';
|
|
66
67
|
function lateEntityObject(states, nodeId) {
|
|
67
68
|
let found;
|
|
68
69
|
for (const state of states) {
|
|
@@ -92,46 +93,28 @@ function owningBody(object, states) {
|
|
|
92
93
|
* `useRapier()`). Renders nothing.
|
|
93
94
|
*/
|
|
94
95
|
export function RapierPhysicsBridge({ onDebugChange }) {
|
|
95
|
-
|
|
96
|
+
// This bridge publishes the module slot below, which the entry's
|
|
97
|
+
// `systems.physics` declaration (`rapierPhysicsSystem()`) forwards to —
|
|
98
|
+
// that declaration is the ONE path the host reads this world's physics
|
|
99
|
+
// through.
|
|
96
100
|
const { colliderStates, rapier, rigidBodyStates, world } = useRapier();
|
|
97
|
-
const registerSystemAdapter = ctx.registerSystemAdapter;
|
|
98
101
|
useEffect(() => {
|
|
99
|
-
if (!registerSystemAdapter)
|
|
100
|
-
return;
|
|
101
102
|
/**
|
|
102
|
-
* node id → the Rapier body driving it,
|
|
103
|
-
*
|
|
103
|
+
* node id → the Rapier body driving it, entirely through the library's
|
|
104
|
+
* OWN body↔object map (`rigidBodyStates`, carried on the `RapierContext`
|
|
105
|
+
* `useRapier()` returns): resolve `userData.entityId` — this stack's
|
|
106
|
+
* identity convention, stamped by the editor's R3F authoring adapter —
|
|
107
|
+
* inside the body-owned subtrees, then walk ANCESTORS to the owning body
|
|
108
|
+
* (`<RigidBody>` owns a subtree, so the selected mesh is usually a
|
|
109
|
+
* descendant of the object the state names).
|
|
104
110
|
*
|
|
105
|
-
*
|
|
106
|
-
* is this stack's identity convention (the editor's R3F authoring
|
|
107
|
-
* adapter stamps it; `../adapter/setup-three-root-adapter.ts` resolves
|
|
108
|
-
* the same way for the imperative root).
|
|
109
|
-
* 2. `Object3D` → body, through `rigidBodyStates` — the library's OWN
|
|
110
|
-
* body↔object map, carried on the `RapierContext` `useRapier()`
|
|
111
|
-
* returns. Walking ANCESTORS matters: `<RigidBody>` owns a subtree, so
|
|
112
|
-
* the selected mesh is usually a descendant of the object the state
|
|
113
|
-
* names.
|
|
114
|
-
*
|
|
115
|
-
* Both maps are live, so this is computed per call rather than cached —
|
|
111
|
+
* The map is live, so this is computed per call rather than cached —
|
|
116
112
|
* calls arrive at gesture rate (a gizmo drag start/end, a dev command),
|
|
117
|
-
* never per frame
|
|
118
|
-
*
|
|
119
|
-
* after this component's first effect (`world-context.ts`'s
|
|
120
|
-
* `setSceneCamera`), so a destructured copy would be permanently absent.
|
|
113
|
+
* never per frame, and only objects under a `<RigidBody>` can have a body
|
|
114
|
+
* at all, so the body subtrees are the complete search space.
|
|
121
115
|
*/
|
|
122
116
|
const bodyFor = (nodeId) => {
|
|
123
|
-
const
|
|
124
|
-
const object = indexed ?? lateEntityObject(rigidBodyStates.values(), nodeId);
|
|
125
|
-
if (!indexed && object) {
|
|
126
|
-
// The editor assigns its stable authoring entity ids AFTER Fiber's
|
|
127
|
-
// first commit, while the root's SceneIndex is constructed during
|
|
128
|
-
// that commit. Resolve the one late id inside the library-owned body
|
|
129
|
-
// subtrees and immediately reindex it; subsequent gestures take the
|
|
130
|
-
// normal indexed path. This is the SceneIndex's documented escape
|
|
131
|
-
// hatch for a userData id written after construction, not a parallel
|
|
132
|
-
// scene mirror.
|
|
133
|
-
ctx.sceneIndex?.reindex(object);
|
|
134
|
-
}
|
|
117
|
+
const object = lateEntityObject(rigidBodyStates.values(), nodeId);
|
|
135
118
|
if (!object)
|
|
136
119
|
return undefined;
|
|
137
120
|
return owningBody(object, rigidBodyStates.values());
|
|
@@ -303,8 +286,58 @@ export function RapierPhysicsBridge({ onDebugChange }) {
|
|
|
303
286
|
joints,
|
|
304
287
|
previewJointAnchor,
|
|
305
288
|
};
|
|
306
|
-
|
|
307
|
-
return () =>
|
|
308
|
-
|
|
289
|
+
liveAdapter = adapter;
|
|
290
|
+
return () => {
|
|
291
|
+
if (liveAdapter === adapter)
|
|
292
|
+
liveAdapter = null;
|
|
293
|
+
};
|
|
294
|
+
}, [colliderStates, onDebugChange, rapier, rigidBodyStates, world]);
|
|
309
295
|
return null;
|
|
310
296
|
}
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// The entry-declaration half
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
/** The mounted bridge's adapter, while one is live. Module-scoped on purpose:
|
|
301
|
+
* the entry's `systems` declaration is static, the rapier world is not, and
|
|
302
|
+
* this slot is the seam between the two. */
|
|
303
|
+
let liveAdapter = null;
|
|
304
|
+
function requireLiveAdapter() {
|
|
305
|
+
if (!liveAdapter) {
|
|
306
|
+
throw new Error('rapierPhysicsSystem: no <RapierPhysicsBridge> is mounted inside a live <Physics> right ' +
|
|
307
|
+
'now — the world is not playing, or the scene never mounted the bridge.');
|
|
308
|
+
}
|
|
309
|
+
return liveAdapter;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The `systems.physics` slot for a default-exported world — declare it from
|
|
313
|
+
* the entry (`export const systems = { physics: rapierPhysicsSystem() }`) and
|
|
314
|
+
* keep `<RapierPhysicsBridge />` mounted inside `<Physics>`. Every call
|
|
315
|
+
* forwards to the LIVE bridge adapter, so the static declaration answers with
|
|
316
|
+
* the running world's physics; while none is mounted, every call refuses by
|
|
317
|
+
* name rather than fabricating.
|
|
318
|
+
*/
|
|
319
|
+
export function rapierPhysicsSystem() {
|
|
320
|
+
// Every forward REQUIRES the live adapter and its member — a silent no-op
|
|
321
|
+
// or fabricated empty answer is the shape the anti-shim rule forbids. The
|
|
322
|
+
// bridge's adapter always carries all five optional members, so the member
|
|
323
|
+
// checks only ever fire against a foreign replacement.
|
|
324
|
+
const requireMember = (member) => {
|
|
325
|
+
const adapter = requireLiveAdapter();
|
|
326
|
+
const value = adapter[member];
|
|
327
|
+
if (typeof value !== 'function') {
|
|
328
|
+
throw new Error(`rapierPhysicsSystem: the live physics adapter has no ${String(member)}().`);
|
|
329
|
+
}
|
|
330
|
+
return value.bind(adapter);
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
ownerOf: (nodeId) => requireLiveAdapter().ownerOf(nodeId),
|
|
334
|
+
freeze: (nodeId) => requireLiveAdapter().freeze(nodeId),
|
|
335
|
+
commit: (nodeId, t) => requireLiveAdapter().commit(nodeId, t),
|
|
336
|
+
unfreeze: (nodeId) => requireLiveAdapter().unfreeze(nodeId),
|
|
337
|
+
setDebugDrawEnabled: (enabled) => requireMember('setDebugDrawEnabled')(enabled),
|
|
338
|
+
colliders: (nodeId) => requireMember('colliders')(nodeId),
|
|
339
|
+
previewCollider: (colliderId, shape) => requireMember('previewCollider')(colliderId, shape),
|
|
340
|
+
joints: (nodeId) => requireMember('joints')(nodeId),
|
|
341
|
+
previewJointAnchor: (jointId, endpoint, anchor) => requireMember('previewJointAnchor')(jointId, endpoint, anchor),
|
|
342
|
+
};
|
|
343
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/engine",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.22",
|
|
6
6
|
"description": "Readable TypeScript game engine and universal host for Three.js, PixiJS, and React games.",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"game-engine",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"id": "input-value-model",
|
|
36
36
|
"area": "input",
|
|
37
37
|
"status": "supported-with-caveats",
|
|
38
|
-
"claim": "F1 (spec §12) added a typed action-value model (digital/scalar/vector2/pointerDelta/pointerPosition, default digital) with real deadzone/combine rules and a valueType-mismatch throw. F2 ('Complete Device Backends') un-rejects mouse_move/gamepad_axis_pair, giving both real readers (mouse_move -> pointerDelta/vector2 off the real mouse-delta accumulator; gamepad_axis_pair -> vector2 off a coupled xAxis/yAxis stick, deadzone-rescaled like gamepad_axis), and adds touch_button/touch_stick (digital/vector2) via setTouchButton/setTouchStick. Real window focus (blur/focus) and pointer-lock state are tracked (requestPointerLock / exitPointerLock) and flush held REAL-device input on loss (#144: virtual/scheduled machine input gates on enabled only and survives blur); gamepad device identity (id/mapping) is exposed via getGamepadInfo/ActionValueSource.gamepadId. F4 adds getLastActiveDevice() + an optional getPrompt device param. Device-poll getters (isDeviceKeyPressed / isDeviceMouseButtonPressed / getDeviceMouseDelta / getDeviceMousePosition / hasDeviceGamepad / isDeviceGamepadButtonPressed / getDeviceGamepadAxis) read the same keysDown/mouseButtons/gamepads stores without inventing implicit actions, and return inert at READ time when inputActive is false. Caveat: pointerPosition still has no real-device backend (test_pointer_position injection only); scalar isn't fed by mouse_move (no axis-select field).",
|
|
38
|
+
"claim": "F1 (spec §12) added a typed action-value model (digital/scalar/vector2/pointerDelta/pointerPosition, default digital) with real deadzone/combine rules and a valueType-mismatch throw. F2 ('Complete Device Backends') un-rejects mouse_move/gamepad_axis_pair, giving both real readers (mouse_move -> pointerDelta/vector2 off the real mouse-delta accumulator; gamepad_axis_pair -> vector2 off a coupled xAxis/yAxis stick, deadzone-rescaled like gamepad_axis), and adds touch_button/touch_stick (digital/vector2) via setTouchButton/setTouchStick. Real window focus (blur/focus) and pointer-lock state are tracked (requestPointerLock / exitPointerLock) and flush held REAL-device input on loss (#144: virtual/scheduled machine input gates on enabled only and survives blur); gamepad device identity (id/mapping) is exposed via getGamepadInfo/ActionValueSource.gamepadId. F4 adds getLastActiveDevice() + an optional getPrompt device param. Device-poll getters (isDeviceKeyPressed / isDeviceMouseButtonPressed / getDeviceMouseDelta / getDeviceMousePosition / hasDeviceGamepad / isDeviceGamepadButtonPressed / getDeviceGamepadAxis) read the same keysDown/mouseButtons/gamepads stores without inventing implicit actions, and return inert at READ time when inputActive is false. getDeviceGamepadAxis is deliberately RAW — no deadzone: the engine's own stick deadzone is on the action path (gamepad_axis / gamepad_axis_pair, where a game declares and can override it), and a device-door caller that wants a stick applies its own engine's processor (unity-compat reproduces Unity's StickDeadzone at 0.125/0.925). Caveat: pointerPosition still has no real-device backend (test_pointer_position injection only); scalar isn't fed by mouse_move (no axis-select field).",
|
|
39
39
|
"evidence": [
|
|
40
40
|
"packages/engine/src/input/input-types.ts:2-40 (InputBinding union incl. mouse_move/gamepad_axis_pair/touch_button/touch_stick/test_*)",
|
|
41
41
|
"packages/engine/src/input/input-manager.ts (digital/gating/typed getters/device-poll/inject*/setTouch*/getGamepadInfo/F4 getLastActiveDevice+getPrompt/requestPointerLock/exitPointerLock)"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"notes": "gamepad disconnect/reconnect needs no special-cased code: poll() re-reads navigator.getGamepads() and every read skips a null slot, so a disconnected gamepad's actions go neutral with a correct isJustReleased edge for free — see input-device-backends-f2.test.ts.",
|
|
44
44
|
"evidenceHashes": {
|
|
45
45
|
"packages/engine/src/input/input-types.ts": "62d9005609a72b55a8214c869d1b27825b1a17b9389f3f3f5d0b04725331b382",
|
|
46
|
-
"packages/engine/src/input/input-manager.ts": "
|
|
46
|
+
"packages/engine/src/input/input-manager.ts": "928a1cde1c7245cfd8afc47794ddf28db8d1368bf7f88e602bb645f79f92ba61"
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
{
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"notes": "Scope is native-DOM React ONLY (D-N1): R3F/@pixi/react games are canvas games and ride the three.js/pixi seams instead. Generality limit: ONE proven game (react-rpg — menu-to-dungeon e2e under React 19; both game-2 candidates rejected on evidence, R-N7). Game deps must be host-installed root deps (D-N6). Open R-N6: vendored react games' raw window listeners bypass the play-mode input gate.",
|
|
89
89
|
"evidenceHashes": {
|
|
90
90
|
"vendor/games/verify-unaltered.mjs": "257a362030a10f4d72a3280815069fbd83f436e1619391635b6f8df2ddd6c339",
|
|
91
|
-
"packages/editor/src/adapter-resolver.ts": "
|
|
91
|
+
"packages/editor/src/adapter-resolver.ts": "63e4204afceb4395f395d5a4df48a5148de75da597a4407ee74ccf233c5d2fa6"
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
{
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The manifest's SOLE INTERPRETER (owner ruling, 2026-08-20 — see
|
|
3
|
+
* docs/ARCHITECTURE-CORE.md §adapter). `vgai.project.json` is static
|
|
4
|
+
* inventory; the adapter seam is the ONE layer that turns it into execution
|
|
5
|
+
* bindings, and host code consumes the manifest through THESE derivations —
|
|
6
|
+
* never by reading `manifest.roots` directly (the guard is
|
|
7
|
+
* `packages/engine/test/manifest-sole-interpreter.test.ts`).
|
|
8
|
+
*
|
|
9
|
+
* Why a doorway and not a convention: one deriver means one direction of
|
|
10
|
+
* truth (manifest → adapter → host). When the project's own `vgai.adapter.ts`
|
|
11
|
+
* later interposes on a derivation, every host path that asked the
|
|
12
|
+
* interpreter gets the project's answer for free; a path that read the raw
|
|
13
|
+
* field would silently keep the mechanical one.
|
|
14
|
+
*
|
|
15
|
+
* The module-free audiences named by the ruling (a bundled CLI's raw
|
|
16
|
+
* discovery over unvalidated files, Vite config time, synchronous
|
|
17
|
+
* dep-optimization, and the loader that CONSTRUCTS the resolution) remain
|
|
18
|
+
* direct readers by design; everything else asks here.
|
|
19
|
+
*
|
|
20
|
+
* These functions are pure and synchronous over an already-loaded manifest —
|
|
21
|
+
* interpreting inventory never evaluates project modules, so any host realm
|
|
22
|
+
* may import this. They are structurally generic so both the loader's
|
|
23
|
+
* `ResolvedGameManifest` and the editor's lenient `EditModeManifest` view
|
|
24
|
+
* flow through the same doorway.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The declared adapter roots, in mount order (the loader already sorted by
|
|
29
|
+
* `zOrder`, ties by declaration order). This is the mount plan, the readiness
|
|
30
|
+
* roster, and the enumeration every other derivation composes from.
|
|
31
|
+
*/
|
|
32
|
+
export function declaredRoots<Root>(manifest: {
|
|
33
|
+
readonly roots: readonly Root[];
|
|
34
|
+
}): readonly Root[] {
|
|
35
|
+
return manifest.roots;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The root declaring `id`, or undefined — never a silent first-wins pick. */
|
|
39
|
+
export function rootById<Root extends { readonly id: string }>(
|
|
40
|
+
manifest: { readonly roots: readonly Root[] },
|
|
41
|
+
id: string,
|
|
42
|
+
): Root | undefined {
|
|
43
|
+
return manifest.roots.find((root) => root.id === id);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether any declared root mounts on `surface` (e.g. the editor's
|
|
47
|
+
* "does this project have a Three world" gates). The parameter is the closed
|
|
48
|
+
* surface union, not `string` — a typo'd literal must stay a compile error,
|
|
49
|
+
* not a silent forever-false. */
|
|
50
|
+
export function hasRootOnSurface(
|
|
51
|
+
manifest: { readonly roots: readonly { readonly surface: string }[] },
|
|
52
|
+
surface: 'three' | 'canvas' | 'dom',
|
|
53
|
+
): boolean {
|
|
54
|
+
return manifest.roots.some((root) => root.surface === surface);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The declared ingest roots (`adapter.type === 'ingest'`), in mount order.
|
|
58
|
+
* Composite-manifest rules (exactly one, ≥2 is a loud named error) belong to
|
|
59
|
+
* the call sites that state them. */
|
|
60
|
+
export function ingestRoots<
|
|
61
|
+
Root extends { readonly adapter: { readonly type: string } },
|
|
62
|
+
>(manifest: { readonly roots: readonly Root[] }): Root[] {
|
|
63
|
+
return manifest.roots.filter((root) => root.adapter.type === 'ingest');
|
|
64
|
+
}
|
|
@@ -20,6 +20,13 @@ import {
|
|
|
20
20
|
getDebugRegistry,
|
|
21
21
|
} from '../runtime/debug-registry';
|
|
22
22
|
import type { Game } from '../runtime/game';
|
|
23
|
+
import {
|
|
24
|
+
CONTRACT_SYSTEM_SLOTS,
|
|
25
|
+
type ContractSurface,
|
|
26
|
+
projectContractSystemAdapters,
|
|
27
|
+
} from './ingest/contract-system-adapters';
|
|
28
|
+
import type { VgaiGameSystemAdapters } from './ingest/game-contract';
|
|
29
|
+
import type { SystemAdapters } from './system-adapter';
|
|
23
30
|
|
|
24
31
|
export const NATIVE_INPUT_VALUE_TYPES = [
|
|
25
32
|
'digital',
|
|
@@ -31,11 +38,31 @@ export const NATIVE_INPUT_VALUE_TYPES = [
|
|
|
31
38
|
export type NativeInputValueType = (typeof NATIVE_INPUT_VALUE_TYPES)[number];
|
|
32
39
|
export type NativeInputValue = boolean | number | { readonly x: number; readonly y: number };
|
|
33
40
|
|
|
41
|
+
/** A table may be a THUNK, evaluated once at install (after every project
|
|
42
|
+
* module has run), so a game whose declarations accrete during module
|
|
43
|
+
* evaluation — the dev-tools registry's `stat()`/`cheat()` lines beside each
|
|
44
|
+
* mechanic — hands a complete snapshot without ordering its own imports
|
|
45
|
+
* around the debugger. */
|
|
46
|
+
type NativeTable<T> = Readonly<Record<string, T>> | (() => Readonly<Record<string, T>>);
|
|
47
|
+
|
|
48
|
+
/** A command is a plain function, or `{ description, run }` when it wants the
|
|
49
|
+
* session's `game.commands()` listing to say what it does. Argument
|
|
50
|
+
* validation stays inside the function — the game's words at the game's
|
|
51
|
+
* door. */
|
|
52
|
+
export type NativeCommandEntry =
|
|
53
|
+
| ((...args: unknown[]) => unknown)
|
|
54
|
+
| { readonly description?: string; readonly run: (...args: unknown[]) => unknown };
|
|
55
|
+
|
|
34
56
|
export interface NativeDebugModule {
|
|
35
|
-
readonly commands?:
|
|
36
|
-
readonly state?:
|
|
57
|
+
readonly commands?: NativeTable<NativeCommandEntry>;
|
|
58
|
+
readonly state?: NativeTable<() => unknown>;
|
|
37
59
|
readonly input?: {
|
|
38
|
-
|
|
60
|
+
/** May be a thunk, read LIVE on every use — an app whose action set loads
|
|
61
|
+
* asynchronously (an input map fetched at boot) answers with what it has
|
|
62
|
+
* NOW rather than freezing the empty pre-load set at install. */
|
|
63
|
+
readonly actions:
|
|
64
|
+
| Readonly<Record<string, NativeInputValueType>>
|
|
65
|
+
| (() => Readonly<Record<string, NativeInputValueType>>);
|
|
39
66
|
readonly set: (action: string, value: NativeInputValue) => void;
|
|
40
67
|
readonly clear: () => void;
|
|
41
68
|
/** Optional native-scheduler operation. The adapter never synthesizes one. */
|
|
@@ -43,6 +70,12 @@ export interface NativeDebugModule {
|
|
|
43
70
|
/** Optional native-scheduler operation. The adapter never adds a host phase. */
|
|
44
71
|
readonly scheduleAtTick?: (tick: number, action: string, value: NativeInputValue) => void;
|
|
45
72
|
};
|
|
73
|
+
/** The app's own EVENT STREAM, bridged into the session's tick-stamped
|
|
74
|
+
* event log at install — the entry-export replacement for the react
|
|
75
|
+
* emit hook. The app owns the emitter; the host only forwards. */
|
|
76
|
+
readonly events?: {
|
|
77
|
+
readonly subscribe: (listener: (event: string, detail?: unknown) => void) => () => void;
|
|
78
|
+
};
|
|
46
79
|
}
|
|
47
80
|
|
|
48
81
|
type NativeInputBinding = NonNullable<NativeDebugModule['input']>;
|
|
@@ -99,6 +132,15 @@ function parseActions(value: unknown): Record<string, NativeInputValueType> {
|
|
|
99
132
|
function parseInput(value: unknown): NativeInputBinding {
|
|
100
133
|
if (!isRecord(value)) throw new Error('debug.input must be an object.');
|
|
101
134
|
rejectUnknownKeys(value, ['actions', 'set', 'clear', 'tap', 'scheduleAtTick'], 'debug.input');
|
|
135
|
+
if (typeof value['actions'] === 'function') {
|
|
136
|
+
// A live table: validated per read (requireAction/the actions source), not
|
|
137
|
+
// here — the whole point is that its content changes after install.
|
|
138
|
+
const set = value['set'];
|
|
139
|
+
const clear = value['clear'];
|
|
140
|
+
if (typeof set !== 'function') throw new Error('debug.input.set must be a function.');
|
|
141
|
+
if (typeof clear !== 'function') throw new Error('debug.input.clear must be a function.');
|
|
142
|
+
return value as unknown as NativeInputBinding;
|
|
143
|
+
}
|
|
102
144
|
const set = value['set'];
|
|
103
145
|
const clear = value['clear'];
|
|
104
146
|
const tap = value['tap'];
|
|
@@ -134,14 +176,28 @@ export function nativeDebugBindingFromEntryModule(
|
|
|
134
176
|
throw new Error(`Root "${rootId}" exports \`debug\`, but it is not an object.`);
|
|
135
177
|
}
|
|
136
178
|
const raw = entryModule['debug'];
|
|
137
|
-
rejectUnknownKeys(raw, ['commands', 'state', 'input'], `Root "${rootId}" debug export`);
|
|
179
|
+
rejectUnknownKeys(raw, ['commands', 'state', 'input', 'events'], `Root "${rootId}" debug export`);
|
|
138
180
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
181
|
+
// A thunk table defers to install time (see {@link NativeTable}); a plain
|
|
182
|
+
// record validates here, where the error can still name the export.
|
|
183
|
+
const parseTable = (value: unknown, at: string) => {
|
|
184
|
+
if (value === undefined) return undefined;
|
|
185
|
+
if (typeof value === 'function') return value as () => Readonly<Record<string, never>>;
|
|
186
|
+
return functionRecord(value, at);
|
|
187
|
+
};
|
|
188
|
+
// Commands allow the `{ description?, run }` entry shape; full validation
|
|
189
|
+
// happens at install (`resolveCommandTable`), where thunks resolve too.
|
|
190
|
+
const parseCommands = (value: unknown, at: string) => {
|
|
191
|
+
if (value === undefined) return undefined;
|
|
192
|
+
if (typeof value === 'function') return value as () => Readonly<Record<string, never>>;
|
|
193
|
+
if (!isRecord(value)) throw new Error(`${at} must be an object of named commands.`);
|
|
194
|
+
return value as Readonly<Record<string, NativeCommandEntry>>;
|
|
195
|
+
};
|
|
196
|
+
const commands = parseCommands(raw['commands'], 'debug.commands');
|
|
197
|
+
const state = parseTable(raw['state'], 'debug.state');
|
|
143
198
|
const input = raw['input'] === undefined ? undefined : parseInput(raw['input']);
|
|
144
|
-
|
|
199
|
+
const events = raw['events'] === undefined ? undefined : parseEvents(raw['events']);
|
|
200
|
+
if (!commands && !state && !input && !events) {
|
|
145
201
|
throw new Error(`Root "${rootId}" exports an empty \`debug\` object.`);
|
|
146
202
|
}
|
|
147
203
|
return {
|
|
@@ -150,10 +206,48 @@ export function nativeDebugBindingFromEntryModule(
|
|
|
150
206
|
...(commands ? { commands } : {}),
|
|
151
207
|
...(state ? { state } : {}),
|
|
152
208
|
...(input ? { input } : {}),
|
|
209
|
+
...(events ? { events } : {}),
|
|
153
210
|
},
|
|
154
211
|
};
|
|
155
212
|
}
|
|
156
213
|
|
|
214
|
+
function parseEvents(value: unknown): NonNullable<NativeDebugModule['events']> {
|
|
215
|
+
if (!isRecord(value) || typeof value['subscribe'] !== 'function') {
|
|
216
|
+
throw new Error('debug.events must be an object with a subscribe(listener) function.');
|
|
217
|
+
}
|
|
218
|
+
return value as unknown as NonNullable<NativeDebugModule['events']>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Resolve a possibly-thunk COMMAND table, validating each entry's shape. */
|
|
222
|
+
function resolveCommandTable(
|
|
223
|
+
table: NativeTable<NativeCommandEntry> | undefined,
|
|
224
|
+
at: string,
|
|
225
|
+
): Readonly<Record<string, NativeCommandEntry>> {
|
|
226
|
+
if (table === undefined) return {};
|
|
227
|
+
const raw = typeof table === 'function' ? table() : table;
|
|
228
|
+
if (!isRecord(raw)) throw new Error(`${at} must be an object of named commands.`);
|
|
229
|
+
for (const [name, entry] of Object.entries(raw)) {
|
|
230
|
+
if (!name) throw new Error(`${at} contains an empty name.`);
|
|
231
|
+
const ok =
|
|
232
|
+
typeof entry === 'function' ||
|
|
233
|
+
(isRecord(entry) && typeof (entry as { run?: unknown }).run === 'function');
|
|
234
|
+
if (!ok) throw new Error(`${at}.${name} must be a function or { description?, run }.`);
|
|
235
|
+
}
|
|
236
|
+
return raw as Readonly<Record<string, NativeCommandEntry>>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Resolve a possibly-thunk table at install time, validating the result. */
|
|
240
|
+
function resolveTable<T>(
|
|
241
|
+
table: NativeTable<T> | undefined,
|
|
242
|
+
at: string,
|
|
243
|
+
): Readonly<Record<string, T>> {
|
|
244
|
+
if (table === undefined) return {};
|
|
245
|
+
if (typeof table === 'function') {
|
|
246
|
+
return functionRecord(table(), at) as Readonly<Record<string, T>>;
|
|
247
|
+
}
|
|
248
|
+
return table;
|
|
249
|
+
}
|
|
250
|
+
|
|
157
251
|
function valueMatches(type: NativeInputValueType, value: NativeInputValue): boolean {
|
|
158
252
|
if (type === 'digital') return typeof value === 'boolean';
|
|
159
253
|
if (type === 'scalar') return typeof value === 'number' && Number.isFinite(value);
|
|
@@ -176,14 +270,19 @@ function installInput(
|
|
|
176
270
|
const trace: Array<{ tick: number; action: string; value: NativeInputValue }> = [];
|
|
177
271
|
let recording = false;
|
|
178
272
|
|
|
273
|
+
// LIVE read on every use — see the `actions` field's own comment.
|
|
274
|
+
const liveActions = (): Readonly<Record<string, NativeInputValueType>> =>
|
|
275
|
+
typeof input.actions === 'function' ? input.actions() : input.actions;
|
|
276
|
+
|
|
179
277
|
const requireAction = (action: string, value: NativeInputValue): NativeInputValueType => {
|
|
180
|
-
const
|
|
278
|
+
const actions = liveActions();
|
|
279
|
+
const type = actions[action];
|
|
181
280
|
if (!type) {
|
|
182
281
|
throw new DebugError(
|
|
183
282
|
'INPUT_ACTION_NOT_FOUND',
|
|
184
283
|
`Native input has no action named "${action}".`,
|
|
185
284
|
{
|
|
186
|
-
registered: Object.keys(
|
|
285
|
+
registered: Object.keys(actions),
|
|
187
286
|
},
|
|
188
287
|
);
|
|
189
288
|
}
|
|
@@ -271,7 +370,7 @@ function installInput(
|
|
|
271
370
|
};
|
|
272
371
|
|
|
273
372
|
registry.setInputActionsSource(binding.rootId, () =>
|
|
274
|
-
Object.entries(
|
|
373
|
+
Object.entries(liveActions()).map(([name, valueType]) => ({ name, valueType })),
|
|
275
374
|
);
|
|
276
375
|
registry.setInputTraceSource(binding.rootId, () => ({
|
|
277
376
|
version: 1,
|
|
@@ -296,12 +395,110 @@ export function installNativeDebugBindings(
|
|
|
296
395
|
throw new Error(`Native debug binding names unmounted root "${binding.rootId}".`);
|
|
297
396
|
}
|
|
298
397
|
const root = registry.forRoot(binding.rootId);
|
|
299
|
-
|
|
398
|
+
const state = resolveTable(binding.debug.state, `Root "${binding.rootId}" debug.state`);
|
|
399
|
+
for (const [name, read] of Object.entries(state)) {
|
|
300
400
|
root.registerStateProvider(name, read, { tier: 'assisted' });
|
|
301
401
|
}
|
|
302
|
-
|
|
303
|
-
|
|
402
|
+
const commands = resolveCommandTable(
|
|
403
|
+
binding.debug.commands,
|
|
404
|
+
`Root "${binding.rootId}" debug.commands`,
|
|
405
|
+
);
|
|
406
|
+
for (const [name, entry] of Object.entries(commands)) {
|
|
407
|
+
const run = typeof entry === 'function' ? entry : entry.run;
|
|
408
|
+
const description = typeof entry === 'function' ? undefined : entry.description;
|
|
409
|
+
root.registerCommand(
|
|
410
|
+
name,
|
|
411
|
+
{ locus: 'client', ...(description === undefined ? {} : { description }) },
|
|
412
|
+
run,
|
|
413
|
+
);
|
|
304
414
|
}
|
|
305
415
|
if (binding.debug.input) installInput(game, registry, binding, binding.debug.input);
|
|
416
|
+
// The app's emitter → the session's tick-stamped event log. The
|
|
417
|
+
// subscription's disposer is deliberately dropped: it lives exactly as
|
|
418
|
+
// long as this Game's registry does.
|
|
419
|
+
binding.debug.events?.subscribe((event, detail) => root.emit(event, detail));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// The native `systems` export — the first-party door onto `SystemAdapters`
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
//
|
|
427
|
+
// The sibling of `debug` on the SAME module surface: a root entry re-exports
|
|
428
|
+
// one `systems` object beside it —
|
|
429
|
+
//
|
|
430
|
+
// export { debug, systems } from './commands';
|
|
431
|
+
//
|
|
432
|
+
// — whose slots are the ONE declarable carrier both realms share,
|
|
433
|
+
// `VgaiGameSystemAdapters` (`ingest/game-contract.ts`; the native engine is
|
|
434
|
+
// the premade 100% implementation of that contract). Validation is the SAME
|
|
435
|
+
// projection the ingest realm uses (`ingest/contract-system-adapters.ts`), so
|
|
436
|
+
// there is one shape law, not two. This door is what retires
|
|
437
|
+
// `ctx.registerSystemAdapter` from component code (ARCHITECTURE-CORE §System
|
|
438
|
+
// adapters: "a project's `vgai.adapter.ts` binds app-owned systems through
|
|
439
|
+
// declared native exports … Components never call `registerSystemAdapter`").
|
|
440
|
+
//
|
|
441
|
+
// Native-realm difference from ingest: a malformed slot THROWS (this is our
|
|
442
|
+
// own code failing its own contract — fail fast), where the ingest projection
|
|
443
|
+
// files a verdict for the coverage report. A `{ present: false, evidence }`
|
|
444
|
+
// slot is accepted as the positive absence it is and binds nothing.
|
|
445
|
+
|
|
446
|
+
/** One root's statically declared, already-validated system-adapter slots. */
|
|
447
|
+
export interface NativeSystemsBinding {
|
|
448
|
+
readonly rootId: string;
|
|
449
|
+
readonly slots: Readonly<Partial<SystemAdapters>>;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Read the single native `systems` export from an already-loaded root module.
|
|
454
|
+
* `surface` is the root's own mount surface when the caller knows it — it
|
|
455
|
+
* feeds the physics keying check (`PHYSICS_KEYING_BY_SURFACE`).
|
|
456
|
+
*/
|
|
457
|
+
export function nativeSystemsBindingFromEntryModule(
|
|
458
|
+
rootId: string,
|
|
459
|
+
entryModule: unknown,
|
|
460
|
+
surface?: ContractSurface | undefined,
|
|
461
|
+
): NativeSystemsBinding | null {
|
|
462
|
+
if (!isRecord(entryModule) || entryModule['systems'] === undefined) return null;
|
|
463
|
+
const raw = entryModule['systems'];
|
|
464
|
+
if (!isRecord(raw)) {
|
|
465
|
+
throw new Error(`Root "${rootId}" exports \`systems\`, but it is not an object.`);
|
|
466
|
+
}
|
|
467
|
+
rejectUnknownKeys(raw, CONTRACT_SYSTEM_SLOTS, `Root "${rootId}" systems export`);
|
|
468
|
+
const projection = projectContractSystemAdapters(
|
|
469
|
+
{ systemAdapters: raw as VgaiGameSystemAdapters },
|
|
470
|
+
surface,
|
|
471
|
+
);
|
|
472
|
+
if (projection.malformed.length > 0) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
`Root "${rootId}" systems export is malformed: ` +
|
|
475
|
+
projection.malformed.map((slot) => `${slot.slot} — ${slot.reason}`).join('; '),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
if (Object.keys(projection.bound).length === 0 && projection.empty.length === 0) {
|
|
479
|
+
throw new Error(`Root "${rootId}" exports an empty \`systems\` object.`);
|
|
480
|
+
}
|
|
481
|
+
return { rootId, slots: projection.bound };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Install every declared binding onto the mounted Game's game-scoped slot
|
|
485
|
+
* table (`Game.installDeclaredSystemAdapters`), where the ordinary
|
|
486
|
+
* `game.systemAdapters` merge picks them up ahead of any lingering component
|
|
487
|
+
* registration for the same root. */
|
|
488
|
+
export function installNativeSystemsBindings(
|
|
489
|
+
game: Game,
|
|
490
|
+
bindings: readonly NativeSystemsBinding[],
|
|
491
|
+
): void {
|
|
492
|
+
if (bindings.length === 0) return;
|
|
493
|
+
const install = game.installDeclaredSystemAdapters;
|
|
494
|
+
if (!install) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
'Cannot install native systems bindings: this Game does not implement ' +
|
|
497
|
+
'installDeclaredSystemAdapters.',
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
for (const binding of bindings) {
|
|
501
|
+
if (Object.keys(binding.slots).length === 0) continue;
|
|
502
|
+
install.call(game, binding.rootId, binding.slots);
|
|
306
503
|
}
|
|
307
504
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* engine's pipeline — an imported Godot 3 GLES2 game does gamma-space lighting with no tonemapper
|
|
18
18
|
* at all, so ACES quietly desaturates and darkens every colour its author picked.
|
|
19
19
|
*
|
|
20
|
-
* So a
|
|
20
|
+
* So a mount may DECLARE the pipeline its world was authored for, and the mounting adapter applies it to the
|
|
21
21
|
* host's renderer for the life of the mount, restoring what it found on dispose. Three properties
|
|
22
22
|
* of that shape are load-bearing:
|
|
23
23
|
*
|
|
@@ -15,19 +15,15 @@
|
|
|
15
15
|
* or the Pixi reconciler.
|
|
16
16
|
*
|
|
17
17
|
* Surface:
|
|
18
|
-
* - `createPixiReactAdapter({ id, content })` — mount a `@pixi/react` tree as
|
|
19
|
-
* a first-party `surface: "canvas"` root under the host's gated loop.
|
|
20
18
|
* - `pixiReactRootFactory` / `resolveCanvasEntryAdapter` — what a canvas entry
|
|
21
|
-
* module MEANS: a default-exported component
|
|
19
|
+
* module MEANS: a default-exported component, mounted as a first-party
|
|
20
|
+
* `surface: "canvas"` root under the host's gated loop. The world's vgai
|
|
21
|
+
* surface is its entry module's static exports
|
|
22
|
+
* (`export { debug, systems } from './commands'`), connected by the host at
|
|
23
|
+
* mount.
|
|
22
24
|
* - `PixiPrimitive` / `adoptNow` — the canvas surface's `<primitive object={…}>`:
|
|
23
25
|
* render a container the game already owns, and place a spawn in the
|
|
24
26
|
* display tree ahead of the commit that renders it.
|
|
25
|
-
* - `useGameContext()` — this root's `CanvasGameContext` (`input`, `debug`,
|
|
26
|
-
* `random`, `clock`, `game`, `roots`) from inside any component in the tree.
|
|
27
|
-
* Behavior in a canvas world is components + hooks, never a class under a
|
|
28
|
-
* string key.
|
|
29
|
-
* - `EngineBridge` / `EngineBridgeValue` — the context contract, exported for
|
|
30
|
-
* adopters building their own bridge.
|
|
31
27
|
*
|
|
32
28
|
* The Pixi runtime itself is NOT re-exported here. A component reaches the live
|
|
33
29
|
* `Application` (and therefore `stage`, `renderer`, `ticker`) with
|
|
@@ -35,17 +31,5 @@
|
|
|
35
31
|
* assets with Pixi's own `Assets` — the library's API, not a second one.
|
|
36
32
|
*/
|
|
37
33
|
|
|
38
|
-
export { EngineBridge, type EngineBridgeValue, useGameContext } from './engine-bridge';
|
|
39
34
|
export { adoptNow, PixiPrimitive, type PixiPrimitiveProps } from './pixi-primitive';
|
|
40
|
-
export {
|
|
41
|
-
type CreatePixiReactAdapterOptions,
|
|
42
|
-
createPixiReactAdapter,
|
|
43
|
-
} from './pixi-react-adapter';
|
|
44
35
|
export { pixiReactRootFactory, resolveCanvasEntryAdapter } from './pixi-react-root-factory';
|
|
45
|
-
export {
|
|
46
|
-
type CanvasGameContext,
|
|
47
|
-
type CanvasRootContextOptions,
|
|
48
|
-
type CanvasRootRuntime,
|
|
49
|
-
createCanvasRootContext,
|
|
50
|
-
DEFAULT_INPUT_MAP_PATH,
|
|
51
|
-
} from './world-context';
|