@vgai/engine 0.5.15 → 0.5.17
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/adapter-module.d.ts +229 -33
- package/dist/adapter/adapter-module.d.ts.map +1 -1
- package/dist/adapter/adapter-module.js +222 -50
- package/dist/adapter/authoring-seam-contract.d.ts +530 -0
- package/dist/adapter/authoring-seam-contract.d.ts.map +1 -0
- package/dist/adapter/authoring-seam-contract.js +162 -0
- package/dist/adapter/authoring.d.ts +237 -50
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/authoring.js +2 -1
- package/dist/adapter/entrypoint-selection-source.d.ts +35 -0
- package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -0
- package/dist/adapter/entrypoint-selection-source.js +151 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +7 -3
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +145 -27
- package/dist/adapter/first-party-systems.d.ts +13 -3
- package/dist/adapter/first-party-systems.d.ts.map +1 -1
- package/dist/adapter/first-party-systems.js +10 -3
- package/dist/adapter/index.d.ts +7 -2
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +4 -0
- package/dist/adapter/ingest/contract-system-adapters.d.ts +1 -1
- package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
- package/dist/adapter/ingest/contract-system-adapters.js +13 -25
- package/dist/adapter/ingest/game-contract-seams.d.ts +214 -0
- package/dist/adapter/ingest/game-contract-seams.d.ts.map +1 -0
- package/dist/adapter/ingest/game-contract-seams.js +62 -0
- package/dist/adapter/ingest/game-contract.d.ts +5 -3
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +4 -1
- package/dist/adapter/ingest/merge-debug-adapters.d.ts +52 -0
- package/dist/adapter/ingest/merge-debug-adapters.d.ts.map +1 -0
- package/dist/adapter/ingest/merge-debug-adapters.js +141 -0
- package/dist/adapter/ingest/observation-debug-adapter.d.ts +56 -0
- package/dist/adapter/ingest/observation-debug-adapter.d.ts.map +1 -0
- package/dist/adapter/ingest/observation-debug-adapter.js +131 -0
- package/dist/adapter/ingest/scene-capture.d.ts +38 -13
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +101 -24
- package/dist/adapter/ingest/visible-capture-window.d.ts +22 -5
- package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
- package/dist/adapter/ingest/visible-capture-window.js +46 -7
- package/dist/adapter/native-debug-module.d.ts +43 -0
- package/dist/adapter/native-debug-module.d.ts.map +1 -0
- package/dist/adapter/native-debug-module.js +230 -0
- package/dist/adapter/object3d-authoring-subject.d.ts +32 -0
- package/dist/adapter/object3d-authoring-subject.d.ts.map +1 -0
- package/dist/adapter/object3d-authoring-subject.js +25 -0
- package/dist/adapter/root-adapter.d.ts +25 -56
- package/dist/adapter/root-adapter.d.ts.map +1 -1
- package/dist/adapter/root-adapter.js +4 -20
- package/dist/adapter/root-seam-contract.d.ts +129 -0
- package/dist/adapter/root-seam-contract.d.ts.map +1 -0
- package/dist/adapter/root-seam-contract.js +37 -0
- package/dist/adapter/seam-evidence.d.ts +118 -0
- package/dist/adapter/seam-evidence.d.ts.map +1 -0
- package/dist/adapter/seam-evidence.js +231 -0
- package/dist/adapter/setup-three-root-adapter.d.ts +2 -3
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.js +11 -12
- package/dist/adapter/system-adapter.d.ts +6 -1
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/adapter/system-seam-contract.d.ts +354 -0
- package/dist/adapter/system-seam-contract.d.ts.map +1 -0
- package/dist/adapter/system-seam-contract.js +95 -0
- package/dist/animation/xstate-animation-binding.d.ts +6 -0
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
- package/dist/animation/xstate-animation-binding.js +142 -4
- package/dist/asset-formats/collider.d.ts +1 -1
- package/dist/asset-formats/mesh.d.ts +2 -2
- package/dist/asset-formats/particles.d.ts +1 -1
- package/dist/canvas-react/engine-bridge.d.ts +6 -6
- package/dist/canvas-react/engine-bridge.d.ts.map +1 -1
- package/dist/canvas-react/engine-bridge.js +2 -2
- package/dist/canvas-react/index.d.ts +2 -2
- package/dist/canvas-react/index.d.ts.map +1 -1
- package/dist/canvas-react/index.js +1 -1
- package/dist/canvas-react/pixi-react-adapter.d.ts +15 -9
- package/dist/canvas-react/pixi-react-adapter.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-adapter.js +129 -64
- package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-root-factory.js +4 -0
- package/dist/canvas-react/world-context.d.ts +22 -12
- package/dist/canvas-react/world-context.d.ts.map +1 -1
- package/dist/canvas-react/world-context.js +12 -51
- package/dist/data/curve.d.ts +1 -1
- package/dist/data/curve.js +2 -2
- package/dist/dev/instruments.d.ts +4 -4
- package/dist/dev/instruments.js +4 -4
- package/dist/dev/register-render-vitals.d.ts +2 -2
- package/dist/dev/register-render-vitals.js +2 -2
- package/dist/dev/static-batch-advisor.d.ts +1 -1
- package/dist/dev/static-batch-advisor.js +2 -2
- package/dist/ecs/user-data.d.ts +5 -0
- package/dist/ecs/user-data.d.ts.map +1 -1
- package/dist/ecs/user-data.js +3 -0
- package/dist/input/input-manager.d.ts +64 -0
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +149 -0
- package/dist/manifest/load.d.ts +0 -3
- package/dist/manifest/load.d.ts.map +1 -1
- package/dist/manifest/load.js +0 -4
- package/dist/manifest/schema.d.ts +9 -11
- package/dist/manifest/schema.d.ts.map +1 -1
- package/dist/manifest/schema.js +2 -23
- package/dist/pixi/authoring.d.ts +33 -0
- package/dist/pixi/authoring.d.ts.map +1 -1
- package/dist/pixi/authoring.js +301 -2
- package/dist/pixi/ingest.d.ts +2 -1
- package/dist/pixi/ingest.d.ts.map +1 -1
- package/dist/pixi/ingest.js +4 -2
- package/dist/pixi/scene-capture.d.ts +9 -1
- package/dist/pixi/scene-capture.d.ts.map +1 -1
- package/dist/pixi/scene-capture.js +9 -6
- package/dist/react/world-state.d.ts +12 -0
- package/dist/react/world-state.d.ts.map +1 -1
- package/dist/react/world-state.js +14 -0
- package/dist/render/particles-factory.d.ts +13 -6
- package/dist/render/particles-factory.d.ts.map +1 -1
- package/dist/render/particles-factory.js +6 -3
- package/dist/render/soft-particle-depth.d.ts +3 -3
- package/dist/render/soft-particle-depth.d.ts.map +1 -1
- package/dist/render/soft-particle-depth.js +8 -13
- package/dist/runtime/create-runtime.d.ts +25 -62
- package/dist/runtime/create-runtime.d.ts.map +1 -1
- package/dist/runtime/create-runtime.js +58 -105
- package/dist/runtime/dev-build.d.ts +37 -0
- package/dist/runtime/dev-build.d.ts.map +1 -0
- package/dist/runtime/dev-build.js +49 -0
- package/dist/runtime/game.d.ts +38 -72
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +40 -55
- package/dist/runtime/mount-game.d.ts.map +1 -1
- package/dist/runtime/mount-game.js +12 -1
- package/dist/runtime/mount-manifest.d.ts +0 -13
- package/dist/runtime/mount-manifest.d.ts.map +1 -1
- package/dist/runtime/mount-manifest.js +5 -25
- package/dist/runtime/types.d.ts +18 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/setup/setup-renderer.d.ts +3 -4
- package/dist/setup/setup-renderer.d.ts.map +1 -1
- package/dist/setup/setup-renderer.js +3 -4
- package/dist/world3d-react/engine-bridge.d.ts +1 -1
- package/dist/world3d-react/engine-bridge.d.ts.map +1 -1
- package/dist/world3d-react/engine-bridge.js +29 -4
- package/dist/world3d-react/index.d.ts +1 -1
- package/dist/world3d-react/index.js +1 -1
- package/dist/world3d-react/r3f-adapter.d.ts +7 -0
- package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
- package/dist/world3d-react/r3f-adapter.js +132 -28
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +14 -0
- package/dist/world3d-react/rapier-physics-bridge.d.ts +4 -4
- package/dist/world3d-react/rapier-physics-bridge.js +3 -3
- package/dist/world3d-react/world-context.d.ts +52 -3
- package/dist/world3d-react/world-context.d.ts.map +1 -1
- package/dist/world3d-react/world-context.js +94 -64
- package/dist-config/config.js.map +1 -1
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +8 -8
- package/schemas/vgai-project.schema.json +1 -6
- package/src/adapter/adapter-module.ts +405 -72
- package/src/adapter/authoring-seam-contract.ts +211 -0
- package/src/adapter/authoring.ts +242 -50
- package/src/adapter/entrypoint-selection-source.ts +159 -0
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +162 -36
- package/src/adapter/first-party-systems.ts +17 -3
- package/src/adapter/index.ts +51 -1
- package/src/adapter/ingest/contract-system-adapters.ts +26 -30
- package/src/adapter/ingest/game-contract-seams.ts +85 -0
- package/src/adapter/ingest/game-contract.ts +9 -4
- package/src/adapter/ingest/merge-debug-adapters.ts +193 -0
- package/src/adapter/ingest/observation-debug-adapter.ts +158 -0
- package/src/adapter/ingest/scene-capture.ts +126 -23
- package/src/adapter/ingest/visible-capture-window.ts +63 -9
- package/src/adapter/native-debug-module.ts +307 -0
- package/src/adapter/object3d-authoring-subject.ts +50 -0
- package/src/adapter/root-adapter.ts +25 -56
- package/src/adapter/root-seam-contract.ts +57 -0
- package/src/adapter/seam-evidence.ts +336 -0
- package/src/adapter/setup-three-root-adapter.ts +13 -15
- package/src/adapter/system-adapter.ts +6 -1
- package/src/adapter/system-seam-contract.ts +121 -0
- package/src/animation/xstate-animation-binding.ts +151 -4
- package/src/canvas-react/engine-bridge.ts +6 -6
- package/src/canvas-react/index.ts +2 -1
- package/src/canvas-react/pixi-react-adapter.tsx +143 -77
- package/src/canvas-react/pixi-react-root-factory.tsx +4 -0
- package/src/canvas-react/world-context.ts +26 -101
- package/src/data/curve.ts +2 -2
- package/src/dev/instruments.ts +4 -4
- package/src/dev/register-render-vitals.ts +2 -2
- package/src/dev/static-batch-advisor.ts +2 -2
- package/src/ecs/user-data.ts +5 -0
- package/src/input/input-manager.ts +145 -0
- package/src/manifest/load.ts +0 -7
- package/src/manifest/schema.ts +2 -25
- package/src/pixi/authoring.ts +329 -2
- package/src/pixi/ingest.ts +9 -2
- package/src/pixi/scene-capture.ts +20 -5
- package/src/react/world-state.tsx +15 -0
- package/src/render/particles-factory.ts +16 -6
- package/src/render/soft-particle-depth.ts +8 -13
- package/src/runtime/create-runtime.ts +79 -158
- package/src/runtime/dev-build.ts +49 -0
- package/src/runtime/game.ts +72 -113
- package/src/runtime/mount-game.ts +19 -1
- package/src/runtime/mount-manifest.ts +5 -37
- package/src/runtime/types.ts +22 -0
- package/src/setup/setup-renderer.ts +3 -4
- package/src/world3d-react/engine-bridge.ts +29 -4
- package/src/world3d-react/index.ts +1 -1
- package/src/world3d-react/r3f-adapter.tsx +140 -31
- package/src/world3d-react/r3f-root-factory.tsx +14 -0
- package/src/world3d-react/rapier-physics-bridge.tsx +4 -4
- package/src/world3d-react/world-context.ts +111 -87
- package/dist/runtime/dev-layers.d.ts +0 -34
- package/dist/runtime/dev-layers.d.ts.map +0 -1
- package/dist/runtime/dev-layers.js +0 -46
- package/src/runtime/dev-layers.ts +0 -46
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects an ADAPTER's declared observation slots
|
|
3
|
+
* (`AdapterDefinition.observation`, `adapter/adapter-module.ts`) onto the host's
|
|
4
|
+
* existing {@link DebugAdapter} — the one seam `game.providers()` /
|
|
5
|
+
* `game.state()` / `game.commands()` already read through
|
|
6
|
+
* (`command-listener.ts`'s `dispatchBridgeMethod` → `getActiveSystems().debug`).
|
|
7
|
+
*
|
|
8
|
+
* This is the SIBLING of `contract-debug-adapter.ts` and deliberately the same
|
|
9
|
+
* shape: a projection rather than a second door. The two differ only in WHO
|
|
10
|
+
* declared the surface — the game itself, on `window.vgaiGame.systems`, or the
|
|
11
|
+
* game's adapter module beside it — and a consumer downstream must not be able
|
|
12
|
+
* to tell them apart. `merge-debug-adapters.ts` is what folds both onto the one
|
|
13
|
+
* slot.
|
|
14
|
+
*
|
|
15
|
+
* ## What `game` is
|
|
16
|
+
*
|
|
17
|
+
* The host's to hand over, and for an INGEST mount it is the REALM the game's
|
|
18
|
+
* own modules ran in. That is not a convenience: an ingest mount runs the game
|
|
19
|
+
* in the editor's own realm (`game-contract.ts`'s `readGameContract` says so —
|
|
20
|
+
* "the realm every ingest mount runs the game's modules in, where the game and
|
|
21
|
+
* the host share one global"), a vendored bundle's adapter is HOST-realm by the
|
|
22
|
+
* placement rule, and the only handles such a game publishes are the ones on
|
|
23
|
+
* that realm — its runtime's own public registry, or a host-build seam beside
|
|
24
|
+
* it. Handing the realm is therefore handing the game everything it actually
|
|
25
|
+
* exposes; handing anything narrower would make the adapter reach for `window`
|
|
26
|
+
* itself, which is the same read with the provenance hidden.
|
|
27
|
+
*
|
|
28
|
+
* ## The two-state rule, kept
|
|
29
|
+
*
|
|
30
|
+
* Declaring a slot and answering it are separate facts. A declaration with NO
|
|
31
|
+
* `answer` is still listed and refuses BY NAME when read — never omitted,
|
|
32
|
+
* because omitting it would report "the game has no such state" when the truth
|
|
33
|
+
* is "the table declared it and nothing binds it yet". Same for a throwing
|
|
34
|
+
* `answer`: it propagates (exactly as a first-party provider's does), and only
|
|
35
|
+
* `stateAll()` isolates it per key.
|
|
36
|
+
*
|
|
37
|
+
* ## What this deliberately does NOT do
|
|
38
|
+
*
|
|
39
|
+
* - **Validate arguments.** A declared command's `answer` receives the invoked
|
|
40
|
+
* argument list after the game handle; there is no Zod tuple beside a foreign
|
|
41
|
+
* game, so a verb validates itself and throws its own error, surfaced wrapped
|
|
42
|
+
* as `DEBUG_COMMAND_FAILED`. Identical to the contract projection's stance.
|
|
43
|
+
* - **Fabricate an event ring.** Nothing emits behind a declaration, so
|
|
44
|
+
* `events()` is empty — accurate, not a stub.
|
|
45
|
+
*/
|
|
46
|
+
import { DebugError } from '../../runtime/debug-registry';
|
|
47
|
+
/**
|
|
48
|
+
* An adapter-declared read is DERIVED by the host-side table from the game's own
|
|
49
|
+
* handles — which is precisely what `assisted` means in the tier vocabulary
|
|
50
|
+
* (`game-contract.ts`: "`observable` is a value the game already computes,
|
|
51
|
+
* `assisted` is one the shim derives"). Reporting `observable` would claim the
|
|
52
|
+
* game itself publishes the value.
|
|
53
|
+
*/
|
|
54
|
+
const DECLARED_TIER = 'assisted';
|
|
55
|
+
function unanswered(id, kind) {
|
|
56
|
+
return new DebugError('OBSERVATION_UNANSWERED', `Observation "${id}" (${kind}) is declared by this game's adapter with no \`answer\` — ` +
|
|
57
|
+
'the slot exists and nothing binds it yet, which is not the same as the game having none.', { id, kind });
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build a {@link DebugAdapter} over an adapter's observation declarations, or
|
|
61
|
+
* `null` when it declares none. `null` is the honest floor — the same one
|
|
62
|
+
* `createContractDebugAdapter` returns — so a game that declared nothing keeps
|
|
63
|
+
* reporting `DEBUG_ADAPTER_UNAVAILABLE` rather than serving an empty adapter
|
|
64
|
+
* that reads as "this game has no state".
|
|
65
|
+
*/
|
|
66
|
+
export function createObservationDebugAdapter(declarations, game) {
|
|
67
|
+
const all = declarations ?? [];
|
|
68
|
+
if (all.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
// Snapshot into id-keyed maps once, exactly as the contract projection does:
|
|
71
|
+
// a later duplicate shadows an earlier one and the listing de-duplicates with
|
|
72
|
+
// it, so a listed id can never resolve differently than it reads.
|
|
73
|
+
const byProvider = new Map(all.filter((d) => d.kind === 'state').map((d) => [d.id, d]));
|
|
74
|
+
const byCommand = new Map(all.filter((d) => d.kind === 'command').map((d) => [d.id, d]));
|
|
75
|
+
return {
|
|
76
|
+
providers: () => [...byProvider.keys()].map((name) => ({ name, tier: DECLARED_TIER })),
|
|
77
|
+
state: (name) => {
|
|
78
|
+
const declaration = byProvider.get(name);
|
|
79
|
+
if (!declaration) {
|
|
80
|
+
throw new DebugError('STATE_PROVIDER_NOT_FOUND', `No state provider named "${name}" is declared by this game's adapter.`, { registered: [...byProvider.keys()] });
|
|
81
|
+
}
|
|
82
|
+
if (!declaration.answer)
|
|
83
|
+
throw unanswered(name, 'state');
|
|
84
|
+
return declaration.answer(game);
|
|
85
|
+
},
|
|
86
|
+
stateAll: () => {
|
|
87
|
+
const out = {};
|
|
88
|
+
for (const [name, declaration] of byProvider) {
|
|
89
|
+
// One throwing declaration must not cost the caller every other
|
|
90
|
+
// reading — the same per-key isolation the first-party registry gives.
|
|
91
|
+
try {
|
|
92
|
+
out[name] = declaration.answer
|
|
93
|
+
? declaration.answer(game)
|
|
94
|
+
: (() => {
|
|
95
|
+
throw unanswered(name, 'state');
|
|
96
|
+
})();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
out[name] = { __error: String(err) };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
},
|
|
104
|
+
commands: () => [...byCommand.keys()].map((name) => ({
|
|
105
|
+
name,
|
|
106
|
+
// An ingested game runs entirely in the browser realm the host mounted
|
|
107
|
+
// it in; there is no server leg for its verbs to route to.
|
|
108
|
+
locus: byCommand.get(name)?.locus ?? 'client',
|
|
109
|
+
})),
|
|
110
|
+
invoke: async (name, args) => {
|
|
111
|
+
const declaration = byCommand.get(name);
|
|
112
|
+
if (!declaration) {
|
|
113
|
+
throw new DebugError('DEBUG_COMMAND_NOT_REGISTERED', `No command named "${name}" is declared by this game's adapter.`, { registered: [...byCommand.keys()] });
|
|
114
|
+
}
|
|
115
|
+
if (!declaration.answer)
|
|
116
|
+
throw unanswered(name, 'command');
|
|
117
|
+
try {
|
|
118
|
+
// The game handle first, then the invoked argument list — a declared
|
|
119
|
+
// verb is a function OF the mounted game, which is the whole reason
|
|
120
|
+
// `answer` takes one.
|
|
121
|
+
return await declaration.answer(game, ...args);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
throw new DebugError('DEBUG_COMMAND_FAILED', `Command "${name}" threw: ${String(err)}`, {
|
|
125
|
+
name,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
events: () => [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scene capture — the core primitive of unmodified-game ingestion.
|
|
3
3
|
*
|
|
4
|
-
* An external three.js game owns its own
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* first renders.
|
|
8
|
-
* `WebGLRenderer.prototype.render`:
|
|
4
|
+
* An external three.js game owns its own renderer, `Scene`, camera, and render
|
|
5
|
+
* loop. To let the editor inspect/edit that live scene WITHOUT touching the
|
|
6
|
+
* game's code, we need a handle to the game's `Scene`+camera the moment it
|
|
7
|
+
* first renders. For WebGL, the robust way to obtain it is an **accessor trap**
|
|
8
|
+
* on `WebGLRenderer.prototype.render`:
|
|
9
9
|
*
|
|
10
10
|
* - `WebGLRenderer` assigns `this.render` as an OWN instance property inside
|
|
11
11
|
* its constructor (not on the prototype), and `THREE.WebGLRenderer` is a
|
|
@@ -20,12 +20,16 @@
|
|
|
20
20
|
* (`docs/ingest-study-spike/vgai-ingest-adapter.js`), proven against an
|
|
21
21
|
* unmodified `three.js/examples/games_fps` game.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
23
|
+
* DOM-backed addon renderers use ordinary prototype methods instead. The host
|
|
24
|
+
* supplies those exact shared classes through `additionalRendererCtors`, and
|
|
25
|
+
* the same observer captures their `(scene, camera)` pair without inventing a
|
|
26
|
+
* second world model. CSS3DRenderer is the first implementer.
|
|
27
|
+
*
|
|
28
|
+
* CRITICAL: every trap must be installed on the SAME `three` module/addon
|
|
29
|
+
* instance the game uses. In a bundler/dev-server that dedupes `three` (one
|
|
30
|
+
* `node_modules/three`), an external ESM game's imports resolve to those shared
|
|
31
|
+
* classes. A game that bundles its own copy cannot be captured this way (the
|
|
32
|
+
* module-identity gatekeeper).
|
|
29
33
|
*
|
|
30
34
|
* Wave 17 adds an OPTIONAL, ADDITIVE composer capture: a game rendering through
|
|
31
35
|
* its own three.js addon `EffectComposer`
|
|
@@ -50,14 +54,35 @@
|
|
|
50
54
|
*/
|
|
51
55
|
import type * as THREE from 'three';
|
|
52
56
|
import { type VisibilityClock, type VisibleCaptureWindow } from './visible-capture-window';
|
|
57
|
+
/** The renderer operations the ingest host may lawfully use after capture.
|
|
58
|
+
* WebGLRenderer supplies every member; DOM-backed Three addon renderers such as
|
|
59
|
+
* CSS3DRenderer deliberately omit the WebGL-only lifecycle operations. */
|
|
60
|
+
export interface CapturedThreeRenderer {
|
|
61
|
+
readonly domElement: HTMLElement;
|
|
62
|
+
readonly info?: THREE.WebGLInfo | undefined;
|
|
63
|
+
render(scene: THREE.Scene, camera: THREE.Camera): unknown;
|
|
64
|
+
setSize(width: number, height: number, updateStyle?: boolean): void;
|
|
65
|
+
setAnimationLoop?(callback: ((time: number) => void) | null): void;
|
|
66
|
+
dispose?(): void;
|
|
67
|
+
getPixelRatio?(): number;
|
|
68
|
+
getContext?(): unknown;
|
|
69
|
+
getRenderTarget?(): unknown;
|
|
70
|
+
}
|
|
53
71
|
/** A live runtime captured from an external game on its first rendered frame. */
|
|
54
72
|
export interface CapturedRuntime {
|
|
55
73
|
scene: THREE.Scene;
|
|
56
74
|
camera: THREE.Camera;
|
|
57
|
-
renderer:
|
|
75
|
+
renderer: CapturedThreeRenderer;
|
|
58
76
|
}
|
|
59
77
|
/** Options for {@link installSceneCapture}. */
|
|
60
78
|
export interface SceneCaptureOptions {
|
|
79
|
+
/**
|
|
80
|
+
* Additional shared Three addon renderer classes whose prototype `render`
|
|
81
|
+
* method carries the same `(scene, camera)` pair as WebGLRenderer. CSS3DRenderer
|
|
82
|
+
* is the first implementer. The caller must pass the exact class its game
|
|
83
|
+
* imports; a bundled private copy remains deliberately uncapturable.
|
|
84
|
+
*/
|
|
85
|
+
additionalRendererCtors?: readonly unknown[];
|
|
61
86
|
/**
|
|
62
87
|
* True when `renderer` is one the HOST constructed for its own drawing.
|
|
63
88
|
*
|
|
@@ -206,7 +231,7 @@ export interface SceneCaptureHandle {
|
|
|
206
231
|
getDrawCount(): number;
|
|
207
232
|
/** The game's last (non-null) `setAnimationLoop` callback for a renderer, so the
|
|
208
233
|
* host can pause (set null) and resume (re-set it) the game's own loop. */
|
|
209
|
-
getAnimationLoop(renderer:
|
|
234
|
+
getAnimationLoop(renderer: CapturedThreeRenderer): ((time: number) => void) | null;
|
|
210
235
|
/**
|
|
211
236
|
* Wave 17 (D-C3): resize every captured `EffectComposer` that renders
|
|
212
237
|
* through the captured renderer to `w`×`h`, matching its pixel ratio to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scene-capture.d.ts","sourceRoot":"","sources":["../../../src/adapter/ingest/scene-capture.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"scene-capture.d.ts","sourceRoot":"","sources":["../../../src/adapter/ingest/scene-capture.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scene capture — the core primitive of unmodified-game ingestion.
|
|
3
3
|
*
|
|
4
|
-
* An external three.js game owns its own
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* first renders.
|
|
8
|
-
* `WebGLRenderer.prototype.render`:
|
|
4
|
+
* An external three.js game owns its own renderer, `Scene`, camera, and render
|
|
5
|
+
* loop. To let the editor inspect/edit that live scene WITHOUT touching the
|
|
6
|
+
* game's code, we need a handle to the game's `Scene`+camera the moment it
|
|
7
|
+
* first renders. For WebGL, the robust way to obtain it is an **accessor trap**
|
|
8
|
+
* on `WebGLRenderer.prototype.render`:
|
|
9
9
|
*
|
|
10
10
|
* - `WebGLRenderer` assigns `this.render` as an OWN instance property inside
|
|
11
11
|
* its constructor (not on the prototype), and `THREE.WebGLRenderer` is a
|
|
@@ -20,12 +20,16 @@
|
|
|
20
20
|
* (`docs/ingest-study-spike/vgai-ingest-adapter.js`), proven against an
|
|
21
21
|
* unmodified `three.js/examples/games_fps` game.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
23
|
+
* DOM-backed addon renderers use ordinary prototype methods instead. The host
|
|
24
|
+
* supplies those exact shared classes through `additionalRendererCtors`, and
|
|
25
|
+
* the same observer captures their `(scene, camera)` pair without inventing a
|
|
26
|
+
* second world model. CSS3DRenderer is the first implementer.
|
|
27
|
+
*
|
|
28
|
+
* CRITICAL: every trap must be installed on the SAME `three` module/addon
|
|
29
|
+
* instance the game uses. In a bundler/dev-server that dedupes `three` (one
|
|
30
|
+
* `node_modules/three`), an external ESM game's imports resolve to those shared
|
|
31
|
+
* classes. A game that bundles its own copy cannot be captured this way (the
|
|
32
|
+
* module-identity gatekeeper).
|
|
29
33
|
*
|
|
30
34
|
* Wave 17 adds an OPTIONAL, ADDITIVE composer capture: a game rendering through
|
|
31
35
|
* its own three.js addon `EffectComposer`
|
|
@@ -48,7 +52,7 @@
|
|
|
48
52
|
* against the `bloom-composer` fixture
|
|
49
53
|
* (`docs/f13-bloom-composer-proof/record-fixed.mjs`).
|
|
50
54
|
*/
|
|
51
|
-
import { startVisibleCaptureWindow, } from './visible-capture-window';
|
|
55
|
+
import { documentVisibilityClock, startVisibleCaptureWindow, } from './visible-capture-window';
|
|
52
56
|
/**
|
|
53
57
|
* How many distinct alternates are recorded before the trap stops looking.
|
|
54
58
|
*
|
|
@@ -112,7 +116,27 @@ export function installSceneCapture(threeNamespace, effectComposerCtor, opts) {
|
|
|
112
116
|
// the GETTER returns a wrapper that records each non-null callback before forwarding.
|
|
113
117
|
const REAL_SAL = Symbol('vgai.realSetAnimationLoop');
|
|
114
118
|
const loopCallbacks = new WeakMap();
|
|
119
|
+
const loopedRenderers = new Set();
|
|
120
|
+
/** Visibility clock of the in-flight `waitForCapture`, if any. */
|
|
121
|
+
let waitingVisibility = null;
|
|
115
122
|
const priorSAL = Object.getOwnPropertyDescriptor(proto, 'setAnimationLoop');
|
|
123
|
+
function pumpHiddenLoops() {
|
|
124
|
+
if (captured ||
|
|
125
|
+
!waitingVisibility ||
|
|
126
|
+
!(waitingVisibility.suspended?.() ?? waitingVisibility.hidden())) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
for (const renderer of loopedRenderers) {
|
|
130
|
+
try {
|
|
131
|
+
loopCallbacks.get(renderer)?.(0);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* a throwing game frame must not kill the waiter */
|
|
135
|
+
}
|
|
136
|
+
if (captured)
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
116
140
|
Object.defineProperty(proto, 'setAnimationLoop', {
|
|
117
141
|
configurable: true,
|
|
118
142
|
set(fn) {
|
|
@@ -121,8 +145,14 @@ export function installSceneCapture(threeNamespace, effectComposerCtor, opts) {
|
|
|
121
145
|
get() {
|
|
122
146
|
const self = this;
|
|
123
147
|
return function setAnimationLoop(cb) {
|
|
124
|
-
if (cb)
|
|
148
|
+
if (cb) {
|
|
125
149
|
loopCallbacks.set(self, cb);
|
|
150
|
+
loopedRenderers.add(self);
|
|
151
|
+
// Hidden tabs park rAF. Play/eval still need a first frame, so when
|
|
152
|
+
// a waiter is parked we drive the game's own loop once — the same
|
|
153
|
+
// class of tick `waitSimTime` already uses for a hidden document.
|
|
154
|
+
queueMicrotask(pumpHiddenLoops);
|
|
155
|
+
}
|
|
126
156
|
const real = self[REAL_SAL];
|
|
127
157
|
return typeof real === 'function'
|
|
128
158
|
? real.call(self, cb)
|
|
@@ -203,6 +233,21 @@ export function installSceneCapture(threeNamespace, effectComposerCtor, opts) {
|
|
|
203
233
|
drawCount,
|
|
204
234
|
});
|
|
205
235
|
}
|
|
236
|
+
/** Observe the common `(scene, camera)` render contract once, regardless of
|
|
237
|
+
* whether it came from WebGLRenderer's constructor-assigned method or an
|
|
238
|
+
* addon's ordinary prototype method. */
|
|
239
|
+
function observeRendererRender(self, scene, camera) {
|
|
240
|
+
if (!captured || self === captured.renderer)
|
|
241
|
+
drawCount++;
|
|
242
|
+
if (opts?.isHostRenderer?.(self) === true)
|
|
243
|
+
return;
|
|
244
|
+
if (!scene?.isScene)
|
|
245
|
+
return;
|
|
246
|
+
if (captured)
|
|
247
|
+
recordAlternateWorld(self, scene, camera);
|
|
248
|
+
else
|
|
249
|
+
adoptWorld(self, scene, camera);
|
|
250
|
+
}
|
|
206
251
|
Object.defineProperty(proto, 'render', {
|
|
207
252
|
configurable: true,
|
|
208
253
|
set(fn) {
|
|
@@ -221,8 +266,6 @@ export function installSceneCapture(threeNamespace, effectComposerCtor, opts) {
|
|
|
221
266
|
// `setAnimationLoop`, so they ARE gateable) and the instrument was
|
|
222
267
|
// reporting someone else's frames. Before capture every render still
|
|
223
268
|
// counts: that is how the first game frame is detected at all.
|
|
224
|
-
if (!captured || self === captured.renderer)
|
|
225
|
-
drawCount++;
|
|
226
269
|
// Never capture a scene the HOST owns. The trap sits on the shared
|
|
227
270
|
// `WebGLRenderer.prototype`, so the editor's own viewport renders
|
|
228
271
|
// arrive here too — and when a game bundles its own MISMATCHED three,
|
|
@@ -233,16 +276,39 @@ export function installSceneCapture(threeNamespace, effectComposerCtor, opts) {
|
|
|
233
276
|
// Whether that happened at all came down to whether the editor
|
|
234
277
|
// rendered a frame inside the game's capture window, so the same
|
|
235
278
|
// session could pass or fail on timing alone.
|
|
236
|
-
|
|
237
|
-
if (captured)
|
|
238
|
-
recordAlternateWorld(self, scene, camera);
|
|
239
|
-
else
|
|
240
|
-
adoptWorld(self, scene, camera);
|
|
241
|
-
}
|
|
279
|
+
observeRendererRender(self, scene, camera);
|
|
242
280
|
return forwardRender(self, args);
|
|
243
281
|
};
|
|
244
282
|
},
|
|
245
283
|
});
|
|
284
|
+
// DOM-backed Three renderers (CSS3DRenderer is the first) use a normal
|
|
285
|
+
// prototype method rather than WebGLRenderer's constructor assignment. Trap
|
|
286
|
+
// only classes explicitly supplied by the host, and restore each byte-for-byte.
|
|
287
|
+
const additionalRendererRestores = [];
|
|
288
|
+
for (const candidate of opts?.additionalRendererCtors ?? []) {
|
|
289
|
+
const ctor = candidate;
|
|
290
|
+
const rendererProto = ctor?.prototype;
|
|
291
|
+
const priorRender = rendererProto?.render;
|
|
292
|
+
if (!rendererProto || typeof priorRender !== 'function' || rendererProto === proto)
|
|
293
|
+
continue;
|
|
294
|
+
rendererProto.render = function (...args) {
|
|
295
|
+
observeRendererRender(this, args[0], args[1]);
|
|
296
|
+
const hooks = renderPassHooks;
|
|
297
|
+
if (hooks === null || !captured || this !== captured.renderer) {
|
|
298
|
+
return priorRender.apply(this, args);
|
|
299
|
+
}
|
|
300
|
+
hooks.before();
|
|
301
|
+
try {
|
|
302
|
+
return priorRender.apply(this, args);
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
hooks.after();
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
additionalRendererRestores.push(() => {
|
|
309
|
+
rendererProto.render = priorRender;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
246
312
|
// ---- Wave 17 (D-C2): additive composer capture, only when a composer ctor
|
|
247
313
|
// was passed. `EffectComposer` methods (including `render`) live on the
|
|
248
314
|
// PROTOTYPE (a plain ES class — the constructor never does `this.render =
|
|
@@ -296,7 +362,9 @@ export function installSceneCapture(threeNamespace, effectComposerCtor, opts) {
|
|
|
296
362
|
// renderer need not even expose that method for this to stay a no-op.
|
|
297
363
|
if (!captured || composers.size === 0)
|
|
298
364
|
return;
|
|
299
|
-
const pixelRatio = captured.renderer.getPixelRatio();
|
|
365
|
+
const pixelRatio = captured.renderer.getPixelRatio?.();
|
|
366
|
+
if (pixelRatio === undefined)
|
|
367
|
+
return;
|
|
300
368
|
for (const composer of composers) {
|
|
301
369
|
composer.setSize(w, h);
|
|
302
370
|
composer.setPixelRatio(pixelRatio);
|
|
@@ -320,20 +388,27 @@ export function installSceneCapture(threeNamespace, effectComposerCtor, opts) {
|
|
|
320
388
|
const i = waiters.indexOf(wrapped);
|
|
321
389
|
if (i >= 0)
|
|
322
390
|
waiters.splice(i, 1);
|
|
391
|
+
waitingVisibility = null;
|
|
323
392
|
opts.onWait?.(null);
|
|
324
393
|
reject(new Error(`Scene capture timed out after ${timeoutMs}ms of VISIBLE time ` +
|
|
325
|
-
`(${Math.round(captureWindow.elapsedHiddenMs())}ms
|
|
326
|
-
'
|
|
394
|
+
`(${Math.round(captureWindow.elapsedHiddenMs())}ms browser-suspended, which is ` +
|
|
395
|
+
'not counted because no frame can be presented) — the game never rendered, ' +
|
|
327
396
|
'or it bundles its own (un-shared) copy of three.'));
|
|
328
397
|
},
|
|
329
398
|
});
|
|
330
399
|
const wrapped = (rt) => {
|
|
331
400
|
captureWindow.cancel();
|
|
401
|
+
waitingVisibility = null;
|
|
332
402
|
opts.onWait?.(null);
|
|
333
403
|
resolve(rt);
|
|
334
404
|
};
|
|
335
405
|
waiters.push(wrapped);
|
|
406
|
+
waitingVisibility = opts.visibility ?? documentVisibilityClock();
|
|
336
407
|
opts.onWait?.(captureWindow);
|
|
408
|
+
// A waiter that starts already-hidden (the normal `vgai play` path
|
|
409
|
+
// against a backgrounded tab) must not wait for a human to foreground
|
|
410
|
+
// it. Pump any loop the game has already registered.
|
|
411
|
+
queueMicrotask(pumpHiddenLoops);
|
|
337
412
|
});
|
|
338
413
|
},
|
|
339
414
|
uninstall() {
|
|
@@ -379,6 +454,8 @@ export function installSceneCapture(threeNamespace, effectComposerCtor, opts) {
|
|
|
379
454
|
// prototype method — no per-instance own-property to restore, unlike
|
|
380
455
|
// the renderer/loop traps above).
|
|
381
456
|
restoreComposerTrap();
|
|
457
|
+
for (const restore of additionalRendererRestores)
|
|
458
|
+
restore();
|
|
382
459
|
},
|
|
383
460
|
};
|
|
384
461
|
}
|
|
@@ -45,6 +45,16 @@ export interface VisibilityClock {
|
|
|
45
45
|
now(): number;
|
|
46
46
|
/** True while the document is hidden (no rAF, so no frame can be captured). */
|
|
47
47
|
hidden(): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* True while the browser is not presenting frames. This is wider than
|
|
50
|
+
* `hidden()`: WebKit also stops rAF for an unfocused window (and can suspend
|
|
51
|
+
* the page entirely) while `document.hidden` still reads false.
|
|
52
|
+
*
|
|
53
|
+
* Optional for compatibility with injected clocks; absent means `hidden()`.
|
|
54
|
+
*/
|
|
55
|
+
suspended?(): boolean;
|
|
56
|
+
/** Why {@link suspended} is true, when the clock can say. */
|
|
57
|
+
suspensionReason?(): 'hidden' | 'unfocused' | 'page-suspended' | null;
|
|
48
58
|
/** Subscribe to visibility transitions; returns the unsubscribe. */
|
|
49
59
|
subscribe(onChange: () => void): () => void;
|
|
50
60
|
}
|
|
@@ -91,18 +101,25 @@ export interface VisibleCaptureWindow {
|
|
|
91
101
|
readonly budgetMs: number;
|
|
92
102
|
/** Visible milliseconds spent so far. */
|
|
93
103
|
elapsedVisibleMs(): number;
|
|
94
|
-
/** Milliseconds this window has spent
|
|
104
|
+
/** Milliseconds this window has spent browser-suspended (not counted). */
|
|
95
105
|
elapsedHiddenMs(): number;
|
|
96
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Whether the document itself is hidden. Kept distinct from browser
|
|
108
|
+
* suspension so status never calls an unfocused, still-visible window hidden.
|
|
109
|
+
*/
|
|
97
110
|
isHidden(): boolean;
|
|
111
|
+
/** Whether the expiry budget is parked because the browser is not presenting frames. */
|
|
112
|
+
isSuspended(): boolean;
|
|
113
|
+
/** The browser condition parking the budget, when observable. */
|
|
114
|
+
suspensionReason(): 'hidden' | 'unfocused' | 'page-suspended' | null;
|
|
98
115
|
/** Stop the timer and drop the visibility listener. Idempotent. */
|
|
99
116
|
cancel(): void;
|
|
100
117
|
}
|
|
101
118
|
/**
|
|
102
119
|
* Start a window that calls `onExpire` after `budgetMs` of VISIBLE time.
|
|
103
|
-
* While
|
|
104
|
-
* background timer cannot fire it late either) and re-armed
|
|
105
|
-
* budget when
|
|
120
|
+
* While browser frame presentation is suspended the timer is disarmed entirely
|
|
121
|
+
* (so a throttled background timer cannot fire it late either) and re-armed
|
|
122
|
+
* with the remaining budget when frame presentation resumes.
|
|
106
123
|
*/
|
|
107
124
|
export declare function startVisibleCaptureWindow(opts: {
|
|
108
125
|
budgetMs: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"visible-capture-window.d.ts","sourceRoot":"","sources":["../../../src/adapter/ingest/visible-capture-window.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,GAAG,IAAI,MAAM,CAAC;IACd,+EAA+E;IAC/E,MAAM,IAAI,OAAO,CAAC;IAClB,oEAAoE;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC7C;AAED;;yEAEyE;AACzE,wBAAgB,uBAAuB,IAAI,eAAe,
|
|
1
|
+
{"version":3,"file":"visible-capture-window.d.ts","sourceRoot":"","sources":["../../../src/adapter/ingest/visible-capture-window.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,GAAG,IAAI,MAAM,CAAC;IACd,+EAA+E;IAC/E,MAAM,IAAI,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,SAAS,CAAC,IAAI,OAAO,CAAC;IACtB,6DAA6D;IAC7D,gBAAgB,CAAC,IAAI,QAAQ,GAAG,WAAW,GAAG,gBAAgB,GAAG,IAAI,CAAC;IACtE,oEAAoE;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC7C;AAED;;yEAEyE;AACzE,wBAAgB,uBAAuB,IAAI,eAAe,CA4CzD;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,GACd,kBAAkB,CAEpB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,kBAAkB,EACzB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,GACd,kBAAkB,CAUpB;AAED,kEAAkE;AAClE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED;2EAC2E;AAC3E,wBAAgB,eAAe,CAAC,KAAK,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,6DAA6D;AAC7D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED,sFAAsF;AACtF,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yCAAyC;IACzC,gBAAgB,IAAI,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,eAAe,IAAI,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,IAAI,OAAO,CAAC;IACpB,wFAAwF;IACxF,WAAW,IAAI,OAAO,CAAC;IACvB,iEAAiE;IACjE,gBAAgB,IAAI,QAAQ,GAAG,WAAW,GAAG,gBAAgB,GAAG,IAAI,CAAC;IACrE,mEAAmE;IACnE,MAAM,IAAI,IAAI,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,iDAAiD;IACjD,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IACnD,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC,GAAG,oBAAoB,CAuDvB"}
|
|
@@ -39,14 +39,50 @@
|
|
|
39
39
|
* honest answer for a headless caller — it has no tab to background. */
|
|
40
40
|
export function documentVisibilityClock() {
|
|
41
41
|
const doc = typeof document === 'undefined' ? null : document;
|
|
42
|
+
const win = doc?.defaultView ?? null;
|
|
43
|
+
let pageSuspended = false;
|
|
44
|
+
const reason = () => {
|
|
45
|
+
if (pageSuspended)
|
|
46
|
+
return 'page-suspended';
|
|
47
|
+
if (doc?.hidden === true)
|
|
48
|
+
return 'hidden';
|
|
49
|
+
// WebKit stops rAF when the browser window becomes inactive, including
|
|
50
|
+
// when another app is in front. Page Visibility can still say `visible`
|
|
51
|
+
// in that state. Focus is a platform fact, not a Safari/UA sniff, and in
|
|
52
|
+
// browsers that keep rendering while unfocused this merely parks the
|
|
53
|
+
// expiry clock until either a frame arrives or focus returns.
|
|
54
|
+
if (doc && typeof doc.hasFocus === 'function' && !doc.hasFocus())
|
|
55
|
+
return 'unfocused';
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
42
58
|
return {
|
|
43
59
|
now: () => (typeof performance === 'undefined' ? Date.now() : performance.now()),
|
|
44
60
|
hidden: () => doc?.hidden === true,
|
|
61
|
+
suspended: () => reason() !== null,
|
|
62
|
+
suspensionReason: reason,
|
|
45
63
|
subscribe(onChange) {
|
|
46
64
|
if (!doc)
|
|
47
65
|
return () => { };
|
|
66
|
+
const onPageHide = () => {
|
|
67
|
+
pageSuspended = true;
|
|
68
|
+
onChange();
|
|
69
|
+
};
|
|
70
|
+
const onPageShow = () => {
|
|
71
|
+
pageSuspended = false;
|
|
72
|
+
onChange();
|
|
73
|
+
};
|
|
48
74
|
doc.addEventListener('visibilitychange', onChange);
|
|
49
|
-
|
|
75
|
+
win?.addEventListener('blur', onChange);
|
|
76
|
+
win?.addEventListener('focus', onChange);
|
|
77
|
+
win?.addEventListener('pagehide', onPageHide);
|
|
78
|
+
win?.addEventListener('pageshow', onPageShow);
|
|
79
|
+
return () => {
|
|
80
|
+
doc.removeEventListener('visibilitychange', onChange);
|
|
81
|
+
win?.removeEventListener('blur', onChange);
|
|
82
|
+
win?.removeEventListener('focus', onChange);
|
|
83
|
+
win?.removeEventListener('pagehide', onPageHide);
|
|
84
|
+
win?.removeEventListener('pageshow', onPageShow);
|
|
85
|
+
};
|
|
50
86
|
},
|
|
51
87
|
};
|
|
52
88
|
}
|
|
@@ -88,15 +124,16 @@ export function visibleRemainingMs(state, now) {
|
|
|
88
124
|
}
|
|
89
125
|
/**
|
|
90
126
|
* Start a window that calls `onExpire` after `budgetMs` of VISIBLE time.
|
|
91
|
-
* While
|
|
92
|
-
* background timer cannot fire it late either) and re-armed
|
|
93
|
-
* budget when
|
|
127
|
+
* While browser frame presentation is suspended the timer is disarmed entirely
|
|
128
|
+
* (so a throttled background timer cannot fire it late either) and re-armed
|
|
129
|
+
* with the remaining budget when frame presentation resumes.
|
|
94
130
|
*/
|
|
95
131
|
export function startVisibleCaptureWindow(opts) {
|
|
96
132
|
const clock = opts.clock ?? documentVisibilityClock();
|
|
97
133
|
const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
98
134
|
const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
|
|
99
|
-
|
|
135
|
+
const suspended = () => clock.suspended?.() ?? clock.hidden();
|
|
136
|
+
let state = beginVisibleBudget(opts.budgetMs, clock.now(), suspended());
|
|
100
137
|
let timer = null;
|
|
101
138
|
let done = false;
|
|
102
139
|
function disarm() {
|
|
@@ -121,7 +158,7 @@ export function startVisibleCaptureWindow(opts) {
|
|
|
121
158
|
const unsubscribe = clock.subscribe(() => {
|
|
122
159
|
if (done)
|
|
123
160
|
return;
|
|
124
|
-
state = applyVisibility(state, clock.now(),
|
|
161
|
+
state = applyVisibility(state, clock.now(), suspended());
|
|
125
162
|
arm();
|
|
126
163
|
});
|
|
127
164
|
arm();
|
|
@@ -129,7 +166,9 @@ export function startVisibleCaptureWindow(opts) {
|
|
|
129
166
|
budgetMs: opts.budgetMs,
|
|
130
167
|
elapsedVisibleMs: () => visibleElapsedMs(state, clock.now()),
|
|
131
168
|
elapsedHiddenMs: () => hiddenElapsedMs(state, clock.now()),
|
|
132
|
-
isHidden: () =>
|
|
169
|
+
isHidden: () => clock.hidden(),
|
|
170
|
+
isSuspended: () => state.hidden,
|
|
171
|
+
suspensionReason: () => state.hidden ? (clock.suspensionReason?.() ?? (clock.hidden() ? 'hidden' : null)) : null,
|
|
133
172
|
cancel() {
|
|
134
173
|
if (done)
|
|
135
174
|
return;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ordinary module surface a native game may expose to its own debugger.
|
|
3
|
+
*
|
|
4
|
+
* A root entry re-exports one `debug` object from the project's command module:
|
|
5
|
+
*
|
|
6
|
+
* export { debug } from './commands';
|
|
7
|
+
*
|
|
8
|
+
* The command module imports no vgai API. Its functions are the application's
|
|
9
|
+
* own functions over its own stores; the native adapter merely projects that
|
|
10
|
+
* existing registry onto the session debug/input doors after the root mounts.
|
|
11
|
+
* Loading through the root entry is load-bearing: the functions close over the
|
|
12
|
+
* exact module graph the mounted application uses, never a separately evaluated
|
|
13
|
+
* copy of its stores.
|
|
14
|
+
*/
|
|
15
|
+
import type { Game } from '../runtime/game';
|
|
16
|
+
export declare const NATIVE_INPUT_VALUE_TYPES: readonly ["digital", "scalar", "vector2", "pointerDelta", "pointerPosition"];
|
|
17
|
+
export type NativeInputValueType = (typeof NATIVE_INPUT_VALUE_TYPES)[number];
|
|
18
|
+
export type NativeInputValue = boolean | number | {
|
|
19
|
+
readonly x: number;
|
|
20
|
+
readonly y: number;
|
|
21
|
+
};
|
|
22
|
+
export interface NativeDebugModule {
|
|
23
|
+
readonly commands?: Readonly<Record<string, (...args: unknown[]) => unknown>>;
|
|
24
|
+
readonly state?: Readonly<Record<string, () => unknown>>;
|
|
25
|
+
readonly input?: {
|
|
26
|
+
readonly actions: Readonly<Record<string, NativeInputValueType>>;
|
|
27
|
+
readonly set: (action: string, value: NativeInputValue) => void;
|
|
28
|
+
readonly clear: () => void;
|
|
29
|
+
/** Optional native-scheduler operation. The adapter never synthesizes one. */
|
|
30
|
+
readonly tap?: (action: string) => void;
|
|
31
|
+
/** Optional native-scheduler operation. The adapter never adds a host phase. */
|
|
32
|
+
readonly scheduleAtTick?: (tick: number, action: string, value: NativeInputValue) => void;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface NativeDebugBinding {
|
|
36
|
+
readonly rootId: string;
|
|
37
|
+
readonly debug: NativeDebugModule;
|
|
38
|
+
}
|
|
39
|
+
/** Read the single native `debug` export from an already-loaded root module. */
|
|
40
|
+
export declare function nativeDebugBindingFromEntryModule(rootId: string, entryModule: unknown): NativeDebugBinding | null;
|
|
41
|
+
/** Project native registries onto the existing session debugger after mount. */
|
|
42
|
+
export declare function installNativeDebugBindings(game: Game, bindings: readonly NativeDebugBinding[]): void;
|
|
43
|
+
//# sourceMappingURL=native-debug-module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-debug-module.d.ts","sourceRoot":"","sources":["../../src/adapter/native-debug-module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE5C,eAAO,MAAM,wBAAwB,8EAM3B,CAAC;AACX,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAC7E,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC;IAC9E,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC;IACzD,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;QACjE,QAAQ,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;QAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;QAC3B,8EAA8E;QAC9E,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;QACxC,gFAAgF;QAChF,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;KAC3F,CAAC;CACH;AAID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;CACnC;AA0ED,gFAAgF;AAChF,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,OAAO,GACnB,kBAAkB,GAAG,IAAI,CAwB3B;AAkID,gFAAgF;AAChF,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,SAAS,kBAAkB,EAAE,GACtC,IAAI,CAkBN"}
|