@volter/project 0.0.0-stage → 0.5.204

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.
Files changed (53) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +18 -3
  4. package/package.json +64 -4
  5. package/schemas/engine-api.json +57 -0
  6. package/schemas/engine-api.md +22 -0
  7. package/schemas/volter-project.schema.json +468 -0
  8. package/schemas/volter-settings.schema.json +149 -0
  9. package/src/adapter/adapter-module.ts +997 -0
  10. package/src/adapter/adapter-surface.ts +34 -0
  11. package/src/adapter/authoring-seam-contract.ts +224 -0
  12. package/src/adapter/authoring.ts +1336 -0
  13. package/src/adapter/binding.ts +445 -0
  14. package/src/adapter/debug-registry-ref.ts +19 -0
  15. package/src/adapter/editor-looks.ts +519 -0
  16. package/src/adapter/finders/finder-result.ts +29 -0
  17. package/src/adapter/finders/registry.ts +116 -0
  18. package/src/adapter/frame-capture.ts +80 -0
  19. package/src/adapter/host-context.ts +114 -0
  20. package/src/adapter/index.ts +209 -0
  21. package/src/adapter/ingest/contract-system-slots.ts +28 -0
  22. package/src/adapter/ingest/game-contract-seams.ts +86 -0
  23. package/src/adapter/ingest/game-contract.ts +506 -0
  24. package/src/adapter/ingest/mount-readiness.ts +140 -0
  25. package/src/adapter/loop-gate-report.ts +53 -0
  26. package/src/adapter/manifest-interpreter.ts +64 -0
  27. package/src/adapter/native-entry-surface.ts +100 -0
  28. package/src/adapter/physics-adapter-2d.ts +30 -0
  29. package/src/adapter/render-audio.ts +20 -0
  30. package/src/adapter/render-memory.ts +34 -0
  31. package/src/adapter/renderer-config.ts +84 -0
  32. package/src/adapter/root-adapter.ts +243 -0
  33. package/src/adapter/root-seam-contract.ts +61 -0
  34. package/src/adapter/root-stacking.ts +67 -0
  35. package/src/adapter/seam-evidence.ts +336 -0
  36. package/src/adapter/system-adapter.ts +865 -0
  37. package/src/adapter/system-seam-contract.ts +138 -0
  38. package/src/adapter/system-slot.ts +301 -0
  39. package/src/adapter/transform.ts +35 -0
  40. package/src/adapter/workspace-arrangement.ts +92 -0
  41. package/src/core/system-phase.ts +52 -0
  42. package/src/inspection.ts +281 -0
  43. package/src/manifest/configuration-kinds.ts +241 -0
  44. package/src/manifest/editor-port.ts +69 -0
  45. package/src/manifest/filename.ts +45 -0
  46. package/src/manifest/kind-modules.ts +58 -0
  47. package/src/manifest/load-file.ts +33 -0
  48. package/src/manifest/load.ts +380 -0
  49. package/src/manifest/locate.ts +55 -0
  50. package/src/manifest/runtime-environment.ts +48 -0
  51. package/src/manifest/schema.ts +690 -0
  52. package/src/settings/keys.ts +152 -0
  53. package/src/settings/schema.ts +151 -0
@@ -0,0 +1,865 @@
1
+ /**
2
+ * System adapters — optional subsystem capability providers hung off a mounted
3
+ * game (`MountedThreeRoot.systems`). Each is a thin capability boundary the editor can
4
+ * coordinate with WITHOUT owning the implementation. The first-party
5
+ * implementers wrap Rapier / Colyseus / recast; an external game supplies its
6
+ * own or none, and the editor degrades per `capabilities`.
7
+ *
8
+ * These are introspection/coordination boundaries, NOT re-implementations: e.g.
9
+ * `NetworkingAdapter` is not a transport, and `PhysicsAdapter` does not replace
10
+ * Rapier — it lets the editor ask "who owns this object" and "freeze it while I
11
+ * edit it".
12
+ */
13
+
14
+ import type { FrameCapture } from './frame-capture';
15
+ import type { PhysicsAdapter2D } from './physics-adapter-2d';
16
+ import type { OfflineAudioRenderer } from './render-audio';
17
+ import type { RenderMemorySnapshot } from './render-memory';
18
+ import type { Transform, TransformOwner } from './transform';
19
+
20
+ export type PhysicsColliderShape =
21
+ | { readonly type: 'cuboid'; readonly halfExtents: readonly [number, number, number] }
22
+ | { readonly type: 'ball'; readonly radius: number }
23
+ | { readonly type: 'capsule'; readonly halfHeight: number; readonly radius: number }
24
+ /**
25
+ * A collider this seam has no plain-data projection for — a trimesh,
26
+ * heightfield, convex hull, cylinder, cone, …
27
+ *
28
+ * IT IS STILL A COLLIDER, and the body still has it. Implementations used to
29
+ * drop these from `colliders()` entirely, so a body whose only collider was a
30
+ * trimesh inspected as a body with NO physics at all. `kind` is the native
31
+ * shape's own name, so the inspector can say WHICH shape it cannot draw
32
+ * rather than implying nothing is there. Nothing may synthesize dimensions
33
+ * for one, and `previewCollider` refuses it.
34
+ */
35
+ | { readonly type: 'unsupported'; readonly kind: string };
36
+
37
+ /** Plain-data projection of one native 3D physics collider. The owning
38
+ * physics implementation keeps the actual collider; editor authoring only
39
+ * receives the geometry needed to inspect and draw it. */
40
+ export interface PhysicsColliderSnapshot {
41
+ readonly id: string;
42
+ readonly shape: PhysicsColliderShape;
43
+ readonly position: readonly [number, number, number];
44
+ readonly rotation: readonly [number, number, number, number];
45
+ /** Absolute world scale used when the native shape was created. Source
46
+ * dimensions divide by this to recover @react-three/rapier's local args. */
47
+ readonly scale: readonly [number, number, number];
48
+ readonly sensor: boolean;
49
+ }
50
+
51
+ export type PhysicsJointType =
52
+ | 'fixed'
53
+ | 'spherical'
54
+ | 'revolute'
55
+ | 'prismatic'
56
+ | 'rope'
57
+ | 'spring'
58
+ | 'generic';
59
+
60
+ /** Plain-data projection of one native 3D impulse joint. Anchors are exposed
61
+ * in both native body-local space (for persistence) and world space (for
62
+ * viewport instruments); the actual joint remains owned by the physics lib. */
63
+ export interface PhysicsJointSnapshot {
64
+ readonly id: string;
65
+ readonly type: PhysicsJointType;
66
+ readonly body1: string;
67
+ readonly body2: string;
68
+ readonly anchor1: readonly [number, number, number];
69
+ readonly anchor2: readonly [number, number, number];
70
+ readonly worldAnchor1: readonly [number, number, number];
71
+ readonly worldAnchor2: readonly [number, number, number];
72
+ readonly body1Position: readonly [number, number, number];
73
+ readonly body2Position: readonly [number, number, number];
74
+ readonly body1Rotation: readonly [number, number, number, number];
75
+ readonly body2Rotation: readonly [number, number, number, number];
76
+ /** The free axis in body-1 local/world space for revolute/prismatic joints. */
77
+ readonly axis?: readonly [number, number, number];
78
+ readonly worldAxis?: readonly [number, number, number];
79
+ readonly limits?: { readonly min: number; readonly max: number };
80
+ readonly contactsEnabled: boolean;
81
+ }
82
+
83
+ /**
84
+ * Physics coordination so the editor can stably edit an object a simulation
85
+ * would otherwise overwrite every frame: `freeze → apply → unfreeze`.
86
+ *
87
+ * Keyed by NODE ID (the `EditorNode` id), not `THREE.Object3D` (P-4). The
88
+ * Object3D keying made this seam structurally unreachable for any non-three
89
+ * world: a Pixi or React world could never expose a physics inspector, because
90
+ * the seam spoke a vocabulary it has no values in — which is why the Pixi
91
+ * surface grew a parallel {@link PhysicsAdapter2D} (`pixi/system-adapters.ts`)
92
+ * instead of implementing this. Substrate-specific resolution (id → the native
93
+ * object) is the IMPLEMENTER's job, done once at its own boundary.
94
+ *
95
+ * ## `keyedBy`, and why the physics slot's union is TAGGED
96
+ *
97
+ * `SystemAdapters['physics']` holds EITHER shape ({@link PhysicsCarrier}),
98
+ * because a carrier addresses the thing being edited in its surface's own
99
+ * vocabulary and the canvas lane's is the display object itself. But the two
100
+ * shapes carry the SAME four member names and are otherwise indistinguishable
101
+ * at runtime, while the registry that holds them
102
+ * (`editor/src/authoring/active-systems.ts`) is GAME-scoped and surface-blind —
103
+ * one object every editor panel reads. An untagged union would therefore be one
104
+ * nothing could narrow, and every three-lane consumer
105
+ * (`getActivePhysics()?.freeze(nodeId)`, `play-mode.ts`'s `physics.commit(id,
106
+ * transform)`) would have to reach it through a cast asserting a fact nobody
107
+ * checked.
108
+ *
109
+ * So `keyedBy` is a real discriminant, and it is REQUIRED on
110
+ * {@link PhysicsAdapter2D} while OPTIONAL here on purpose: node-id keying is
111
+ * this seam's original and — until the canvas lane — only vocabulary, so every
112
+ * existing implementer stays valid untouched, and the shape that needs telling
113
+ * apart is the one obliged to say so. {@link nodeKeyedPhysics} /
114
+ * {@link displayKeyedPhysics} are the two narrowings; nothing else should test
115
+ * the tag by hand.
116
+ */
117
+ export interface PhysicsAdapter {
118
+ /** Optional tag; see the interface comment. `'node-id'` is the only value,
119
+ * and omitting it means the same thing. */
120
+ readonly keyedBy?: 'node-id';
121
+ /** Returns `'physics'` when a body drives this node, else another owner. */
122
+ ownerOf(nodeId: string): TransformOwner;
123
+ /** Pause the body driving `nodeId` (kinematic / sleep / detach) for editing. */
124
+ freeze(nodeId: string): void;
125
+ /** Teleport the body to the edited pose (so the sim continues from there). */
126
+ commit(nodeId: string, t: Transform): void;
127
+ /** Resume simulation of `nodeId`. */
128
+ unfreeze(nodeId: string): void;
129
+ /** Optional debug-draw object (collider wireframes), in the surface's own
130
+ * medium; opaque to the neutral contract. */
131
+ debugDraw?(): unknown;
132
+ /**
133
+ * Optional: turn the engine's per-frame physics debug rendering on/off —
134
+ * the first-party implementation drives Rapier's `debugRender()` into the
135
+ * `debugDraw()` object inside the game's own render system, so an editor
136
+ * overlay toggle costs the editor zero per-frame work (W2a §2.3c).
137
+ */
138
+ setDebugDrawEnabled?(enabled: boolean): void;
139
+ /**
140
+ * Optional: live world-space contact points this frame, as xyz triples.
141
+ * Returns a view over a REUSED backing buffer — valid only until the next
142
+ * call (no per-frame allocation churn; callers copy into their own
143
+ * geometry).
144
+ */
145
+ contactPoints?(): Float32Array;
146
+ /** Optional native collider inspection for one authored node. Implementers
147
+ * return only shapes they can describe exactly; absence means the editor
148
+ * offers no collider component UI. */
149
+ colliders?(nodeId: string): readonly PhysicsColliderSnapshot[];
150
+ /** Optional gesture preview against the implementation's real collider.
151
+ * Source persistence remains the AuthoringAdapter's responsibility. */
152
+ previewCollider?(colliderId: string, shape: PhysicsColliderShape): void;
153
+ /** Optional native impulse-joint inspection for joints attached to one node. */
154
+ joints?(nodeId: string): readonly PhysicsJointSnapshot[];
155
+ /** Optional gesture preview against the implementation's real local anchor. */
156
+ previewJointAnchor?(
157
+ jointId: string,
158
+ endpoint: 0 | 1,
159
+ anchor: readonly [number, number, number],
160
+ ): void;
161
+ }
162
+
163
+ export interface NetPeer {
164
+ id: string;
165
+ label?: string;
166
+ }
167
+
168
+ /** The project's declared multiplayer server, readable with the game NOT
169
+ * running. `null` from `getServerConfig` means no server is declared. */
170
+ export interface NetServerConfig {
171
+ endpoint: string;
172
+ /** Known once the game's own networking adapter reports it; in edit mode
173
+ * the declared `process` gives only the endpoint. */
174
+ roomName?: string;
175
+ }
176
+
177
+ /** The signed-in player's authored identity, used as the connect default. */
178
+ export interface NetPlayerIdentity {
179
+ name?: string;
180
+ id?: string;
181
+ }
182
+
183
+ /** The live connection lifecycle, for the editor's network inspector. */
184
+ export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
185
+
186
+ /** Identity of the room the local peer is currently in, or `null` when not connected. */
187
+ export interface RoomInfo {
188
+ roomId: string;
189
+ sessionId: string;
190
+ roomName: string;
191
+ }
192
+
193
+ /** Lightweight replication counters for the editor's network inspector. */
194
+ export interface ReplicationStats {
195
+ /** Number of replicated entities currently known (e.g. players + NPCs). */
196
+ entities: number;
197
+ /** Approximate inbound replication activity (state changes observed per second). */
198
+ msgsInPerSec: number;
199
+ /** Approximate outbound message rate (client → server sends per second). */
200
+ msgsOutPerSec: number;
201
+ }
202
+
203
+ export type Unsubscribe = () => void;
204
+
205
+ /** One captured network message, for the inspector's message log (W3b).
206
+ * `seq` is an adapter-lifetime MONOTONIC counter — the ring-buffer fence a
207
+ * consumer polls with (`messageEvents(sinceSeq)`), mirroring
208
+ * `DebugAdapter.events`' seq discipline. `time` is a `performance.now()`-
209
+ * domain ms timestamp at capture. */
210
+ export interface NetMessageEvent {
211
+ seq: number;
212
+ time: number;
213
+ direction: 'in' | 'out';
214
+ /** Message type/channel (e.g. `'input'`, `'players.add'`, `'state.phase'`). */
215
+ type: string;
216
+ /** Approximate payload size in bytes, when measurable. OMITTED — never
217
+ * fabricated — when the transport doesn't expose it (e.g. Colyseus schema
218
+ * patches observed via the Callbacks API carry no byte count). */
219
+ size?: number;
220
+ /** `true` when the link conditioner (`setConditioning`) dropped this
221
+ * outbound message instead of sending it — logged so the inspector can
222
+ * show the drop honestly rather than silently losing it. */
223
+ dropped?: boolean;
224
+ }
225
+
226
+ /** Send/receive rates for the inspector's sparklines (W3b). The byte fields
227
+ * are OPTIONAL capabilities-within-the-capability: an implementer omits a
228
+ * direction it cannot measure (never reports a fabricated 0). */
229
+ /** One message type's traffic, both directions. */
230
+ export interface NetTypeTraffic {
231
+ type: string;
232
+ countIn: number;
233
+ countOut: number;
234
+ bytesIn: number;
235
+ bytesOut: number;
236
+ }
237
+
238
+ /** One replicated entity's traffic (Godot's synchronizer table): the state patches that changed
239
+ * it, the field changes they carried, and the size range of those patches, which carried other
240
+ * entities too. `path` is the entity's place in the state (`players.abc`, `orbs.orb_1`). */
241
+ export interface NetEntityTraffic {
242
+ path: string;
243
+ syncs: number;
244
+ changes: number;
245
+ patchBytesMin: number;
246
+ patchBytesMax: number;
247
+ }
248
+
249
+ export interface NetServerRoom {
250
+ roomId: string;
251
+ name: string;
252
+ clients: number;
253
+ /** `null` when the room admits any number. */
254
+ maxClients: number | null;
255
+ locked: boolean;
256
+ elapsedMs: number;
257
+ }
258
+
259
+ export interface NetServerClient {
260
+ sessionId: string;
261
+ elapsedMs: number;
262
+ }
263
+
264
+ export interface NetServerInspection {
265
+ rooms: NetServerRoom[];
266
+ connections: number;
267
+ /** The server machine's CPU use in percent, when it reports one. */
268
+ cpuPercent: number | null;
269
+ memory: { usedMb: number; totalMb: number } | null;
270
+ /** The current room's clients and its full state's size, when the room is still hosted. */
271
+ room: { roomId: string; clients: NetServerClient[]; stateBytes: number; state?: unknown } | null;
272
+ }
273
+
274
+ export interface NetConditioningLimits {
275
+ latencyMs?: string;
276
+ jitterMs?: string;
277
+ packetLoss?: string;
278
+ }
279
+
280
+ export interface NetRates {
281
+ msgsInPerSec: number;
282
+ msgsOutPerSec: number;
283
+ bytesInPerSec?: number;
284
+ bytesOutPerSec?: number;
285
+ }
286
+
287
+ /** Client-side link-conditioning parameters, applied AT THE ADAPTER SEAM
288
+ * (W3b M3) — the implementer wraps its own transport calls (e.g. delaying/
289
+ * dropping around `room.send`); nobody patches transport internals. */
290
+ export interface NetConditioning {
291
+ /** Fixed added delay per outbound message, ms. */
292
+ latencyMs: number;
293
+ /** Uniform random extra delay in `[0, jitterMs)`, ms. */
294
+ jitterMs: number;
295
+ /** Probability in `[0, 1]` that an outbound message is dropped. */
296
+ packetLoss: number;
297
+ }
298
+
299
+ /**
300
+ * INTROSPECTION over replication state — NOT a networking transport.
301
+ *
302
+ * The optional members are CAPABILITIES in the `SystemAdapters` sense
303
+ * (absence means "not supported" and the editor's Network inspector marks
304
+ * that section absent — it never fabricates data): `getStateSnapshot` feeds
305
+ * the replicated-state tree, `messageEvents` the message log, `getRates` the
306
+ * rate sparklines, and `getConditioning`/`setConditioning` the latency/loss
307
+ * conditioner. The first-party Colyseus factory
308
+ * (`createColyseusNetworkingAdapter`) only attaches each one when its config
309
+ * supplies the accessor, so partial implementers degrade honestly.
310
+ */
311
+ export interface NetworkingAdapter {
312
+ peers(): NetPeer[];
313
+ /** Keyed by NODE ID, not `THREE.Object3D` — see {@link PhysicsAdapter} (P-4). */
314
+ networkId(nodeId: string): string | null;
315
+ authority(nodeId: string): 'local' | 'remote' | 'server';
316
+ /** Remote/server-authoritative nodes → inspect-only in the editor. */
317
+ editable(nodeId: string): boolean;
318
+ /**
319
+ * Optional capability: current connection lifecycle state.
320
+ *
321
+ * OPTIONAL for the same reason every other member here is — `'disconnected'`
322
+ * is a POSITIVE CLAIM about the link, and an implementer that never taught
323
+ * this adapter to read connection state has not made it. Absent ⇒ the
324
+ * inspector marks the section unsupported; it does not report a game as
325
+ * disconnected on the strength of nobody having looked.
326
+ */
327
+ getConnectionState?(): ConnectionState;
328
+ /**
329
+ * Optional capability: the active room's identity, or `null` when connected
330
+ * to no room. `null` is the ANSWER "there is no room", so absence of the
331
+ * accessor — "nothing here can tell you" — has to be a different thing.
332
+ */
333
+ getRoomInfo?(): RoomInfo | null;
334
+ /**
335
+ * Optional capability: replication activity snapshot, for a live network
336
+ * inspector panel. Zeroes are a measurement, not a default; an adapter with
337
+ * no counters omits this rather than reporting a quiet link.
338
+ */
339
+ getReplicationStats?(): ReplicationStats;
340
+ /** Subscribe to changes in connection state / room / replication stats. */
341
+ subscribe(cb: () => void): Unsubscribe;
342
+ /** Optional capability: one plain-data (JSON-safe) snapshot of the
343
+ * replicated room state, or `null` when not connected. Colyseus schema is
344
+ * self-describing (`state.toJSON()`), so the first-party implementation
345
+ * is one call — the inspector renders whatever tree comes back. */
346
+ getStateSnapshot?(): unknown;
347
+ /** Optional capability: captured message events with `seq > sinceSeq`
348
+ * (whole ring when omitted). Implementations ring-buffer (drop oldest);
349
+ * the inspector keeps its own consumer-side ring and fences on `seq`. */
350
+ messageEvents?(sinceSeq?: number): NetMessageEvent[];
351
+ /** Optional capability: msg + byte rates for the inspector's sparklines. */
352
+ getRates?(): NetRates;
353
+ /** Optional capability (paired with `setConditioning`): current
354
+ * conditioning parameters. */
355
+ getConditioning?(): NetConditioning;
356
+ /** Optional capability: apply link conditioning at the adapter seam. */
357
+ setConditioning?(c: NetConditioning): void;
358
+ /** Optional edit-time capability: the project's declared server, readable
359
+ * with the game NOT running (`null` = no server declared). Hides on
360
+ * absence, like the other optionals. */
361
+ getServerConfig?(): NetServerConfig | null;
362
+ /** Optional capability: the local player's identity, as the GAME defines it
363
+ * (e.g. read off its own room state). The editor consumes this for display —
364
+ * a seat label — ONLY when a real implementer provides it, falling back to a
365
+ * generic label otherwise. */
366
+ getPlayerIdentity?(): NetPlayerIdentity | undefined;
367
+ /** Traffic per message type since the connection opened, both directions — state and patches
368
+ * included as their own rows (Godot's network profiler tables). */
369
+ getTrafficByType?(): NetTypeTraffic[];
370
+ /** Incoming state traffic per replicated entity since the connection opened (or the last
371
+ * clear), ordered by syncs. */
372
+ getTrafficByEntity?(): NetEntityTraffic[];
373
+ /** Whether the traffic totals and the message log are counting (Godot's profiler Start/Stop);
374
+ * stopped, frames still pass and the state still updates, but nothing is tallied. */
375
+ isRecording?(): boolean;
376
+ setRecording?(on: boolean): void;
377
+ /** Send `payload` to the room as this client, under message type `type` (Colyseus Monitor's
378
+ * Send), so a server handler can be exercised from the inspector. */
379
+ sendMessage?(type: string, payload: unknown): void;
380
+ /** Which conditioning fields this adapter cannot apply, each with the reason (a reliable
381
+ * WebSocket loses nothing, so it cannot simulate loss). Omitted means every field applies. */
382
+ getConditioningLimits?(): NetConditioningLimits;
383
+ /** Measure one round trip to the server now, in milliseconds. */
384
+ ping?(): Promise<number>;
385
+ /** The room server's own view — every room it hosts, and the current room's clients (Colyseus
386
+ * Monitor's). `null` when the server serves no such view. */
387
+ inspectServer?(roomId?: string): Promise<NetServerInspection | null>;
388
+ /** Disconnect one client of the current room from the server's side (Monitor's Disconnect). */
389
+ disconnectClient?(sessionId: string, roomId?: string): Promise<void>;
390
+ /** Set one value of the room's authoritative state on the server (Monitor's State edit);
391
+ * `path` is the keys from the state's root. */
392
+ editServerState?(path: readonly (string | number)[], value: unknown, roomId?: string): Promise<void>;
393
+ /** Remove one key of the room's authoritative state on the server (Monitor's State delete). */
394
+ deleteServerState?(path: readonly (string | number)[], roomId?: string): Promise<void>;
395
+ /** From the server, send `payload` under `type` to one client of the room (Monitor's Send). */
396
+ sendToClient?(sessionId: string, type: string, payload: unknown, roomId?: string): Promise<void>;
397
+ /** From the server, send `payload` under `type` to every client of the room (Monitor's
398
+ * Broadcast). */
399
+ broadcast?(type: string, payload: unknown, roomId?: string): Promise<void>;
400
+ /** Dispose a room on the server (Monitor's Dispose); its clients are disconnected. */
401
+ disposeRoom?(roomId?: string): Promise<void>;
402
+ /** The replicated type of the state field at `path` (`float32`, `map<schema>`), from the
403
+ * schema the server declared — what Godot's Replication panel shows of a synchronizer. */
404
+ stateFieldType?(path: readonly (string | number)[]): string | null;
405
+ /** Start the traffic totals from zero (Godot's profiler Clear resets its tables). */
406
+ clearTraffic?(): void;
407
+ /** Optional capability, PAIRED with {@link getPlayerIdentity}: set the local
408
+ * player's identity through the game's OWN multiplayer mechanism. The editor
409
+ * renders an editable name field ONLY when a real implementer provides this
410
+ * (consume-by-presence, exactly like {@link setConditioning}); its absence
411
+ * means the game does not support an author-settable name and no field shows.
412
+ *
413
+ * This is NOT the editor imposing a name: whether a name is settable, and how
414
+ * it reaches the server, is entirely the game's decision — a game that names
415
+ * players by join order simply never provides this member. When it does, the
416
+ * implementation drives the game's real path (a join option honoured by the
417
+ * server, a rename message, …), so what the editor sets is the same value the
418
+ * multiplayer build actually uses — never a placeholder the server discards. */
419
+ setPlayerIdentity?(identity: NetPlayerIdentity): void | Promise<void>;
420
+ }
421
+
422
+ export interface NavPoint {
423
+ x: number;
424
+ y: number;
425
+ z: number;
426
+ }
427
+
428
+ /**
429
+ * Navmesh bake parameters — the recast `rcConfig` vocabulary the scene schema
430
+ * (`SceneNavigationSchema`) also mirrors. All optional; an implementation
431
+ * applies its own defaults. `walkableHeight`/`walkableClimb`/`walkableRadius`
432
+ * are in VOXEL units (multiples of `cellHeight`/`cellSize`), per recast.
433
+ */
434
+ export interface NavBakeParams {
435
+ cellSize?: number | undefined;
436
+ cellHeight?: number | undefined;
437
+ walkableSlopeAngle?: number | undefined;
438
+ walkableHeight?: number | undefined;
439
+ walkableClimb?: number | undefined;
440
+ walkableRadius?: number | undefined;
441
+ maxEdgeLen?: number | undefined;
442
+ maxSimplificationError?: number | undefined;
443
+ minRegionArea?: number | undefined;
444
+ mergeRegionArea?: number | undefined;
445
+ }
446
+
447
+ /** One crowd agent's live state — plain data (no engine/WASM handles), for the
448
+ * editor's play-mode crowd debug draw. */
449
+ export interface NavCrowdAgentState {
450
+ position: NavPoint;
451
+ velocity: NavPoint;
452
+ radius: number;
453
+ height: number;
454
+ /** The crowd's own agent state. `invalid` is the expressible FAILURE — the
455
+ * agent is off the navmesh (a bad spawn or teleport) and will not move;
456
+ * `offmesh` is an off-mesh connection traversal. Absent on an adapter that
457
+ * predates this field — the editor draws such agents as walking. */
458
+ state?: 'invalid' | 'walking' | 'offmesh';
459
+ /** The agent's current move target, when the implementation reports one. */
460
+ target?: NavPoint;
461
+ /** The local path-corridor corners the agent is steering along — the
462
+ * REQUESTED PATH the debug draw projects. Empty or absent when idle. */
463
+ corners?: NavPoint[];
464
+ }
465
+
466
+ /**
467
+ * Inspect a game's navigation mesh: whether one is built, query a path, and get a
468
+ * debug overlay. Coordination/introspection only — the editor doesn't own the build.
469
+ *
470
+ * The optional members are CAPABILITIES in the `SystemAdapters` sense (absence
471
+ * means "not supported", the editor degrades): `bake`/`exportData` let the
472
+ * editor's Navigation panel (re)build and persist a navmesh through the seam —
473
+ * the first-party recast implementation (`createNavigationAdapter`) is the
474
+ * blessed implementer — while an external game that owns its build simply
475
+ * omits them. `clear` lets that same owner release its navmesh; the editor never
476
+ * disposes implementation handles itself. `crowdAgents` feeds the play-mode
477
+ * crowd debug draw.
478
+ *
479
+ * `debugMesh`/`bake`/`clear` trade in the surface's own scene, objects and
480
+ * meshes, which this neutral contract leaves opaque; `@volter/editor-threejs`
481
+ * names the Three-typed view (`threeNavigation`).
482
+ */
483
+ export interface NavigationAdapter {
484
+ hasNavMesh(): boolean;
485
+ /**
486
+ * `[]` means SEARCHED AND FOUND NO ROUTE — a fact about the level. An
487
+ * implementer with no navmesh to search, or whose query FAILED, THROWS
488
+ * instead; those are facts about the implementer, and collapsing all three
489
+ * into `[]` tells a caller the level is impassable when nobody looked.
490
+ * Gate on {@link hasNavMesh} where the first case is reachable.
491
+ */
492
+ findPath(start: NavPoint, end: NavPoint): NavPoint[];
493
+ debugMesh(scene: unknown): unknown;
494
+ /** (Re)build the navmesh from source meshes. Synchronous in the blessed
495
+ * recast/WASM implementation; returns `false` on a failed bake. */
496
+ bake?(meshes: readonly unknown[], params?: NavBakeParams): boolean;
497
+ /** Serialize the current navmesh to a binary blob (the editor persists it
498
+ * as the scene's `.navmesh` sidecar). Throws when nothing is built. */
499
+ exportData?(): Uint8Array;
500
+ /** Release the current navmesh and any debug object it attached to `scene`.
501
+ * Optional because an external game's navigation may be inspect-only. */
502
+ clear?(scene: unknown): void;
503
+ /** Live crowd-agent snapshots for debug draw. Empty when no crowd. */
504
+ crowdAgents?(): NavCrowdAgentState[];
505
+ }
506
+
507
+ /** One node in an audio-graph snapshot (W3c, the Audio debugger tab). Edges
508
+ * point DOWNSTREAM: `outputs` holds the ids of nodes this node's output
509
+ * feeds (ultimately reaching the `destination` node). */
510
+ export interface AudioGraphNode {
511
+ id: string;
512
+ /** Node class/constructor name (`'GainNode'`, `'Gain'` for a Tone node,
513
+ * `'AudioDestinationNode'`, …) — whatever the implementation truthfully
514
+ * knows, never a prettified fabrication. */
515
+ type: string;
516
+ /** Human-facing label (`'Master'`, `'Music bus'`) when one exists. */
517
+ label?: string;
518
+ /** ids of downstream nodes this node's output connects to. */
519
+ outputs: string[];
520
+ /** Lifecycle/playback state when truthfully known (`'running'`,
521
+ * `'suspended'`, `'started'`…). OMITTED — never fabricated — otherwise. */
522
+ state?: string;
523
+ }
524
+
525
+ /** Musical-transport snapshot for the Audio debugger's transport strip
526
+ * (W3c). The first-party implementation reads Tone's real transport. */
527
+ export interface AudioTransportState {
528
+ state: 'started' | 'stopped' | 'paused';
529
+ /** Playback position along the transport's timeline, seconds. */
530
+ seconds: number;
531
+ bpm: number;
532
+ /** Musical position (`Bars:Beats:Sixteenths`) when a musical grid exists. */
533
+ position?: string;
534
+ }
535
+
536
+ /** One bus level sample: `level` is linear RMS over the implementation's most
537
+ * recent analysis window (0 = silence; ~1 = full-scale). */
538
+ export interface AudioMeterFrame {
539
+ id: string;
540
+ label: string;
541
+ level: number;
542
+ }
543
+
544
+ /** A live metering session. Metering costs real audio nodes (Tone.Meter /
545
+ * AnalyserNode taps), so it is ACQUIRED for exactly as long as a meter UI is
546
+ * visible and MUST be released via `dispose()` — the first-party handle
547
+ * disconnects its analyser taps there (no leaked nodes after Stop). */
548
+ export interface AudioMeterHandle {
549
+ /** Current level per bus. Cheap enough to poll at UI rate (~10 Hz). */
550
+ read(): AudioMeterFrame[];
551
+ /** Release the metering nodes. Idempotent. */
552
+ dispose(): void;
553
+ }
554
+
555
+ /** One entry in the audio event ring (W3c). `seq` is an adapter-lifetime
556
+ * MONOTONIC counter — the same ring-buffer fence discipline as
557
+ * `NetMessageEvent.seq` / `DebugAdapter.events`. `time` is a
558
+ * `performance.now()`-domain ms timestamp at capture. `'error'` is reserved
559
+ * for implementations that can truthfully report one (the first-party
560
+ * adapter has no honest audio-error source today and never emits it). */
561
+ export interface AudioDebugEvent {
562
+ seq: number;
563
+ time: number;
564
+ kind:
565
+ | 'transport-start'
566
+ | 'transport-stop'
567
+ | 'transport-pause'
568
+ | 'context-statechange'
569
+ | 'mute'
570
+ | 'unmute'
571
+ | 'error';
572
+ detail?: string;
573
+ }
574
+
575
+ /**
576
+ * Silence/restore a world's own audio (D10, T7.6) — NOT a mixer/bus API. This
577
+ * is the seam `Game.play.pause()` calls so a paused world's audio goes quiet
578
+ * too ("an audio seam in `SystemAdapters` so pause can silence game audio
579
+ * (howler et al.) or explicitly report it cannot"). Absence on a world's
580
+ * `mounted.systems` means exactly that: `Game.play.pause()` reports it loudly
581
+ * once per world rather than silently leaving that world's audio playing under
582
+ * a "paused" game.
583
+ *
584
+ * The OPTIONAL methods below are the W3c read-only INTROSPECTION capabilities
585
+ * feeding the editor's Audio debugger tab (graph / transport / meters /
586
+ * events). They follow the W3a degradation ladder: an implementer omits what
587
+ * it cannot truthfully provide, and the editor marks that section absent —
588
+ * never fabricated. The full MIXER (sends/effects/gain editing) is SQ-4-gated
589
+ * and deliberately NOT part of this seam. The first-party implementation over
590
+ * the engine's own Tone/Web-Audio stack is
591
+ * `packages/game-runtime/src/audio/audio-introspection.ts`; pre-existing
592
+ * mute-only registrants keep compiling untouched (every addition is
593
+ * optional).
594
+ */
595
+ export interface AudioAdapter {
596
+ /** Resume/unlock this adapter's real audio context from a user gesture.
597
+ * Optional for external adapters that do not own a resumable Web Audio
598
+ * context; first-party edit and play adapters both provide it. */
599
+ resume?(): void;
600
+ /** `true` silences this world's audio; `false` restores it to whatever
601
+ * level it was at before silencing (an implementer's own concern — the
602
+ * first-party adapter restores its master-gain value, not a hardcoded 1). */
603
+ setMuted(muted: boolean): void;
604
+ /** Current muted state, for inspection (editor mute UI, proofs). */
605
+ isMuted(): boolean;
606
+ /** Optional: current audio-graph snapshot (active nodes + downstream
607
+ * edges). The first-party adapter reports the engine bus hierarchy plus
608
+ * every Tone node routed through `connectToneBusToMasterGain` — it cannot
609
+ * see a game's private Tone-internal wiring and does not pretend to. */
610
+ graphSnapshot?(): AudioGraphNode[];
611
+ /** Optional: musical-transport snapshot. Returns `null` — an honest
612
+ * "this world has no musical transport" — when none is active (e.g. the
613
+ * first-party adapter when the game never bridged Tone onto this world's
614
+ * context). Method ABSENT means the capability itself is unsupported. */
615
+ transportState?(): AudioTransportState | null;
616
+ /** Optional: begin a per-bus metering session. Returns `null` when the
617
+ * environment cannot meter (headless world with no real AudioContext) —
618
+ * the UI says so rather than showing frozen zeros. */
619
+ acquireMeters?(): AudioMeterHandle | null;
620
+ /** Optional: tap the real mixed output for a gameplay recording. The
621
+ * returned stream is the implementation's native browser audio stream; the
622
+ * caller must dispose the tap when recording ends. Absence means video can
623
+ * still be recorded, honestly without an audio track. */
624
+ acquireRecordingStream?(): AudioRecordingHandle | null;
625
+ /**
626
+ * Optional: render this world's audio for the ABSOLUTE/canonical sim range
627
+ * `[start, end)` OFFLINE and deterministically — the fixed-step sibling of
628
+ * `acquireRecordingStream` above. That one taps the speakers in WALL time,
629
+ * which is exactly what the editor's fixed-step video export
630
+ * (`packages/editor-game/src/host/gameplay-export.ts`) may not do: a paused run that
631
+ * is stepped frame by frame emits no real-time audio at all, and anything
632
+ * captured from a live context would vary run to run. This returns PCM for
633
+ * a sim window instead, so identical `(start, end)` on an unchanged score
634
+ * must return identical samples.
635
+ *
636
+ * THE OFFSET TRAP (the gameplay export states it at its call site; quoted
637
+ * here because this is the other place implementers read):
638
+ * an offline render of `[10, 10.72)` schedules its first event at LOCAL
639
+ * time 0, not absolute 10. The caller reasons in canonical time, so the
640
+ * implementation must subtract `start` from every scheduled event time.
641
+ * The export places the returned buffer at local timestamp 0 of the clip.
642
+ *
643
+ * CLAMP THE SUBTRACTION AT ZERO. Measured 2026-09-19 against a working
644
+ * implementation: a sim clock ACCUMULATES its fixed step, so `start` for
645
+ * tick 900 arrives as 15.000000000000036, not 15. An event authored at
646
+ * exactly 15 then schedules at -3.6e-14 and Web Audio throws outright
647
+ * ("Time must be a finite non-negative number"). An event at or
648
+ * infinitesimally before the window start belongs at local 0.
649
+ *
650
+ * Absent — the honest default, and what the first-party master-gain adapter
651
+ * reports — means the export writes a video-only file and says `audio:
652
+ * false` in its result. A live Web Audio graph cannot be re-rendered
653
+ * offline by a bus wrapper; a world that wants export audio owns a
654
+ * deterministic score and binds this through `editor/volter.adapter.ts` (with the
655
+ * `music` capability that is one line over `renderToneOffline`).
656
+ */
657
+ renderOffline?: OfflineAudioRenderer;
658
+ /** Optional: the audio event ring (bounded, drops oldest). `sinceSeq`
659
+ * filters to `seq > sinceSeq` — the consumer's fence. */
660
+ audioEvents?(sinceSeq?: number): AudioDebugEvent[];
661
+ }
662
+
663
+ /** One temporary tap of an audio adapter's mixed output. */
664
+ export interface AudioRecordingHandle {
665
+ readonly stream: MediaStream;
666
+ dispose(): void;
667
+ }
668
+
669
+ /** One native camera known to a runtime camera controller.
670
+ *
671
+ * `id` is the controller's own stable key (a shot name, virtual-camera id,
672
+ * etc.). `nativeId` is OPTIONAL identity from the underlying scene object —
673
+ * Three.js implementations normally use `Object3D.uuid`; another adapter may
674
+ * use its renderer's equivalent. The editor uses it only to correlate an
675
+ * already-selected native camera with this read-only runtime projection. */
676
+ export interface CameraRuntimeCamera {
677
+ id: string;
678
+ label: string;
679
+ nativeId?: string;
680
+ /** Optional because ownership/cut controllers do not necessarily arbitrate
681
+ * by priority. A camera brain that does may expose the real value it uses. */
682
+ priority?: number;
683
+ enabled?: boolean;
684
+ }
685
+
686
+ /** A transition the controller is evaluating now. Plain data only: no
687
+ * renderer camera, tween, graph node, or editor-authored state crosses the
688
+ * seam. */
689
+ export interface CameraRuntimeTransition {
690
+ fromCameraId: string | null;
691
+ toCameraId: string;
692
+ durationSeconds: number;
693
+ elapsedSeconds: number;
694
+ progress: number;
695
+ }
696
+
697
+ /** Read-only runtime state for a selected native camera's Inspector section.
698
+ * Runtime follow/aim/collision stays project behavior; this snapshot merely
699
+ * reports what that behavior already decided. */
700
+ export interface CameraRuntimeSnapshot {
701
+ cameras: readonly CameraRuntimeCamera[];
702
+ activeCameraId: string | null;
703
+ /** The behavior/cinematic currently allowed to drive the rendered camera. */
704
+ activeControllerId: string | null;
705
+ ownershipDepth: number;
706
+ transition: CameraRuntimeTransition | null;
707
+ }
708
+
709
+ /** Camera-controller INTROSPECTION — not a camera graph or controller API.
710
+ *
711
+ * Games continue to author ordinary native cameras and drive them with their
712
+ * own TS/TSX. A controller registers this adapter only when it can truthfully
713
+ * expose its active camera, priority and blend state. Absence hides the
714
+ * runtime UI; the editor never infers a brain from scene structure. */
715
+ export interface CameraAdapter {
716
+ snapshot(): CameraRuntimeSnapshot;
717
+ subscribe(listener: () => void): Unsubscribe;
718
+ }
719
+
720
+ /** One tick-stamped debug event (`ctx.debug.emit`, spec §3.3): `tick`/`simT`
721
+ * are the engine's own counters AT EMISSION, so events and state reads
722
+ * correlate frame-exactly across every door (in-page bridge, relay, panels).
723
+ *
724
+ * `seq` (run-4 friction #5 fix) is a registry-lifetime MONOTONIC counter,
725
+ * strictly increasing by one per `emit()` call regardless of `tick` —
726
+ * unlike `tick`, it is never shared by two events, which is what makes it
727
+ * safe to fence on. A debug command handler runs BETWEEN ticks (its
728
+ * emissions carry whatever `tick` is current at that instant, same as every
729
+ * other emission at that instant would), so two DIFFERENT events — or an
730
+ * event and a consumer's fence point — can legitimately share one `tick`;
731
+ * `seq` never collides the same way, so "everything after what I've already
732
+ * observed" is unambiguous. See `events(sinceSeq)` below. */
733
+ export interface TickStampedEvent {
734
+ tick: number;
735
+ simT: number;
736
+ event: string;
737
+ detail?: unknown;
738
+ seq: number;
739
+ }
740
+
741
+ /** One registered debug command's listing shape — `locus` is always present
742
+ * (defaults to `'client'` when a command didn't declare one) so every
743
+ * listing surface can print it without a fallback of its own. */
744
+ export interface DebugCommandInfo {
745
+ name: string;
746
+ description?: string | undefined;
747
+ /** JSON-Schema projection of the command's Zod args tuple, when declared. */
748
+ argsJsonSchema?: unknown;
749
+ locus: 'client' | 'server';
750
+ }
751
+
752
+ /**
753
+ * The debug/synthetic-player seam: game-scoped introspection + actuation
754
+ * over whatever a game registers via `ctx.debug`
755
+ * (`registerStateProvider`/`registerCommand`/`emit`). NOT a gameplay API —
756
+ * this is the one seam the debug bridge, the editor's Debug Console/State
757
+ * Watch panels, and `@volter/game-live` all read/drive through.
758
+ */
759
+ export interface DebugAdapter {
760
+ providers(): { name: string; tier: 'observable' | 'assisted' }[];
761
+ /** One provider's current value. Throws `DebugError` code
762
+ * `STATE_PROVIDER_NOT_FOUND` (`data.registered`) for an unknown name. */
763
+ state(name: string): unknown;
764
+ /** One coherent snapshot of every registered provider — a throwing
765
+ * provider contributes `{ __error: String(err) }` for its own key rather
766
+ * than failing the whole snapshot. */
767
+ stateAll(): Record<string, unknown>;
768
+ commands(): DebugCommandInfo[];
769
+ /** Validates `args` against the command's declared Zod tuple (when
770
+ * present), then awaits the registered fn. Throws `DebugError` code
771
+ * `DEBUG_COMMAND_NOT_REGISTERED` for an unknown name,
772
+ * `DEBUG_COMMAND_ARGS_INVALID` for a validation failure, or
773
+ * `DEBUG_COMMAND_FAILED` wrapping anything the command itself threw. */
774
+ invoke(name: string, args: unknown[]): Promise<unknown>;
775
+ /** The tick-stamped event ring (cap 500, drops oldest).
776
+ *
777
+ * - `sinceSeq` given (run-4 friction #5 fix): filters to `seq > sinceSeq`
778
+ * — the unambiguous fence for "everything emitted after what I've
779
+ * already observed" (see `TickStampedEvent.seq`'s doc comment for why
780
+ * `tick` alone can't do this: a debug-command emission and a consumer's
781
+ * fence point can share one tick).
782
+ * - Omitted: the whole ring.
783
+ *
784
+ * There was a second, KNOWN-DEFECTIVE `sinceTick` parameter (`tick >
785
+ * sinceTick`, which silently dropped same-tick events) kept for
786
+ * back-compat after the seq fix. It is REMOVED — a fence is `seq` or
787
+ * nothing. */
788
+ events(sinceSeq?: number): TickStampedEvent[];
789
+ }
790
+
791
+ /**
792
+ * Read-only render-debugging introspection (W4b, F11 frame debugger +
793
+ * profiler) — NOT a renderer. `captureFrame` arms the first-party WebGL2
794
+ * capture (`dev/webgl-frame-capture.ts`) and resolves with the NEXT rendered
795
+ * frame's draw list, attributed by wrapping each renderable's `onBeforeRender`
796
+ * (three calls it immediately before that object's main-pass draw; shadow/
797
+ * composer-pass draws stay unattributed BY CONSTRUCTION — an honest gap, not a
798
+ * guess). It REJECTS — never fabricates — if no frame renders within a bounded
799
+ * timeout (a paused/stopped world). `memorySnapshot` is the W3a-style OPTIONAL
800
+ * capability: present only when the mount can supply `renderer.info` (a
801
+ * headless mount omits it, and the editor marks the section absent).
802
+ */
803
+ export interface RenderDebugAdapter {
804
+ captureFrame(): Promise<FrameCapture>;
805
+ memorySnapshot?(): RenderMemorySnapshot;
806
+ }
807
+
808
+ /**
809
+ * A physics carrier in EITHER surface's vocabulary — node-id keyed for a three
810
+ * world, display-object keyed for a canvas one. Tagged by `keyedBy`; see
811
+ * {@link PhysicsAdapter}'s comment for why, and use the two narrowings below
812
+ * rather than reading the tag directly.
813
+ */
814
+ export type PhysicsCarrier = PhysicsAdapter | PhysicsAdapter2D;
815
+
816
+ /** The carrier a NODE-ID-keyed consumer may call, or `null` — including when
817
+ * the slot holds a display-keyed carrier, which such a consumer must not
818
+ * call and cannot correctly address. */
819
+ export function nodeKeyedPhysics(
820
+ carrier: PhysicsCarrier | null | undefined,
821
+ ): PhysicsAdapter | null {
822
+ if (!carrier) return null;
823
+ return isDisplayKeyedPhysics(carrier) ? null : carrier;
824
+ }
825
+
826
+ /**
827
+ * Who drives `key`, asked of the game's own physics carrier by an editor surface that renders the
828
+ * answer. A declared system slot with nothing attached yet (a world whose `<Physics>` mounts late)
829
+ * refuses by name, and a refusal read from an outliner row would take the editor's chrome down with
830
+ * it: to the editor a carrier that cannot answer does not know the node, which `'unresolved'` says.
831
+ */
832
+ export function physicsOwnerOf<K, O extends string>(carrier: { ownerOf(key: K): O }, key: K): O | 'unresolved' {
833
+ try {
834
+ return carrier.ownerOf(key);
835
+ } catch {
836
+ return 'unresolved';
837
+ }
838
+ }
839
+
840
+ /** The carrier a DISPLAY-keyed consumer may call, or `null`. Mirror of
841
+ * {@link nodeKeyedPhysics}. */
842
+ export function displayKeyedPhysics(
843
+ carrier: PhysicsCarrier | null | undefined,
844
+ ): PhysicsAdapter2D | null {
845
+ if (!carrier) return null;
846
+ return isDisplayKeyedPhysics(carrier) ? carrier : null;
847
+ }
848
+
849
+ function isDisplayKeyedPhysics(carrier: PhysicsCarrier): carrier is PhysicsAdapter2D {
850
+ return carrier.keyedBy === 'display';
851
+ }
852
+
853
+ /**
854
+ * The set of optional subsystem providers a mounted game may expose. Absence of
855
+ * a provider means "capability not supported" — the editor degrades gracefully.
856
+ */
857
+ export interface SystemAdapters {
858
+ physics?: PhysicsCarrier;
859
+ networking?: NetworkingAdapter;
860
+ navigation?: NavigationAdapter;
861
+ audio?: AudioAdapter;
862
+ camera?: CameraAdapter;
863
+ debug?: DebugAdapter;
864
+ renderDebug?: RenderDebugAdapter;
865
+ }