@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,53 @@
1
+ /**
2
+ * Capability-gate shortfall reporting (D10, T7.6) — the honesty half of the
3
+ * play-control contract. §(d) draws the `loop` axis (`gated | self-driven`) on
4
+ * its own: a `self-driven` world (an ingested game
5
+ * driving its own rAF/ticker) pauses for real ONLY if its adapter implements an
6
+ * explicit loop-gate capability (`MountedRootBase.setPaused`/`step`) —
7
+ * otherwise it "honestly reports so" rather than a silent no-op (the
8
+ * D5 §5.2 finding: gating a raw-rAF loop from outside was
9
+ * demonstrated and REJECTED). The audio seam (`SystemAdapters.AudioAdapter`)
10
+ * has the identical shape: absent or unable to silence a world's audio on pause
11
+ * must report, not pretend.
12
+ *
13
+ * Pure compute + message-formatting — callers own the actual
14
+ * `console.warn`/`editorConsole.warn` call; this module never logs.
15
+ */
16
+
17
+ /** Greppable prefix for a loop-gate shortfall (pause requested, world's loop
18
+ * could not actually be gated). */
19
+ export const LOOP_GATE_PREFIX = 'loop-gate';
20
+
21
+ /** Greppable prefix for an audio-gate shortfall (pause requested, world's
22
+ * audio could not actually be silenced). */
23
+ export const AUDIO_GATE_PREFIX = 'audio-gate';
24
+
25
+ /** One shortfall instance: which world, and why the capability wasn't there. */
26
+ export interface CapabilityGateReport {
27
+ readonly worldId: string;
28
+ readonly reason: string;
29
+ }
30
+
31
+ function formatGateMessage(
32
+ prefix: string,
33
+ capability: string,
34
+ report: CapabilityGateReport,
35
+ ): string {
36
+ return (
37
+ `${prefix} world "${report.worldId}": pause requested but ${capability} cannot be gated ` +
38
+ `(${report.reason}) — reporting honestly instead of silently no-op-ing. ` +
39
+ `${prefix} ${JSON.stringify(report)}`
40
+ );
41
+ }
42
+
43
+ /** Format a loop-gate shortfall message (a `self-driven` world with no
44
+ * working pause capability). */
45
+ export function formatLoopGateMessage(report: CapabilityGateReport): string {
46
+ return formatGateMessage(LOOP_GATE_PREFIX, "this world's loop", report);
47
+ }
48
+
49
+ /** Format an audio-gate shortfall message (a world with no `AudioAdapter`, or
50
+ * whose adapter cannot actually silence it). */
51
+ export function formatAudioGateMessage(report: CapabilityGateReport): string {
52
+ return formatGateMessage(AUDIO_GATE_PREFIX, "this world's audio", report);
53
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The manifest's SOLE INTERPRETER (owner ruling, 2026-08-20 — see
3
+ * docs/ARCHITECTURE-CORE.md §adapter). `volter.project.json` is static
4
+ * inventory; the adapter seam is the ONE layer that turns it into execution
5
+ * bindings, and host code consumes the manifest through THESE derivations —
6
+ * never by reading `manifest.roots` directly (the guard is
7
+ * `packages/engine/test/manifest-sole-interpreter.test.ts`).
8
+ *
9
+ * Why a doorway and not a convention: one deriver means one direction of
10
+ * truth (manifest → adapter → host). When the project's own `editor/volter.adapter.ts`
11
+ * later interposes on a derivation, every host path that asked the
12
+ * interpreter gets the project's answer for free; a path that read the raw
13
+ * field would silently keep the mechanical one.
14
+ *
15
+ * The module-free audiences named by the ruling (a bundled CLI's raw
16
+ * discovery over unvalidated files, Vite config time, synchronous
17
+ * dep-optimization, and the loader that CONSTRUCTS the resolution) remain
18
+ * direct readers by design; everything else asks here.
19
+ *
20
+ * These functions are pure and synchronous over an already-loaded manifest —
21
+ * interpreting inventory never evaluates project modules, so any host realm
22
+ * may import this. They are structurally generic so both the loader's
23
+ * `ResolvedGameManifest` and the editor's lenient `EditModeManifest` view
24
+ * flow through the same doorway.
25
+ */
26
+
27
+ /**
28
+ * The declared adapter roots, in mount order (the loader already sorted by
29
+ * `zOrder`, ties by declaration order). This is the mount plan, the readiness
30
+ * roster, and the enumeration every other derivation composes from.
31
+ */
32
+ export function declaredRoots<Root>(manifest: {
33
+ readonly roots: readonly Root[];
34
+ }): readonly Root[] {
35
+ return manifest.roots;
36
+ }
37
+
38
+ /** The root declaring `id`, or undefined — never a silent first-wins pick. */
39
+ export function rootById<Root extends { readonly id: string }>(
40
+ manifest: { readonly roots: readonly Root[] },
41
+ id: string,
42
+ ): Root | undefined {
43
+ return manifest.roots.find((root) => root.id === id);
44
+ }
45
+
46
+ /** Whether any declared root mounts on `surface` (e.g. the editor's
47
+ * "does this project have a Three world" gates). The parameter is the closed
48
+ * surface union, not `string` — a typo'd literal must stay a compile error,
49
+ * not a silent forever-false. */
50
+ export function hasRootOnSurface(
51
+ manifest: { readonly roots: readonly { readonly surface: string }[] },
52
+ surface: 'three' | 'canvas' | 'dom',
53
+ ): boolean {
54
+ return manifest.roots.some((root) => root.surface === surface);
55
+ }
56
+
57
+ /** The declared ingest roots (`adapter.type === 'ingest'`), in mount order.
58
+ * Composite-manifest rules (exactly one, ≥2 is a loud named error) belong to
59
+ * the call sites that state them. */
60
+ export function ingestRoots<
61
+ Root extends { readonly adapter: { readonly type: string } },
62
+ >(manifest: { readonly roots: readonly Root[] }): Root[] {
63
+ return manifest.roots.filter((root) => root.adapter.type === 'ingest');
64
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * THE ENTRY MODULE'S DECLARED SURFACE — what a project's root entry may export
3
+ * for its own debugger, and the already-validated bindings a host hands on.
4
+ *
5
+ * This is the contract, not an implementation: a root entry writes
6
+ *
7
+ * export { debug } from './commands';
8
+ *
9
+ * and the command module imports no Volter API — its functions are the
10
+ * application's own functions over its own stores. The READER that validates
11
+ * one of these exports, and the adapter that projects it onto the session's
12
+ * debug/input doors, are the game runtime's
13
+ * (`@volter/editor-game/runtime/adapter/native-debug-module`).
14
+ */
15
+
16
+ import type { ContractSystemEmptySlot } from './ingest/contract-system-slots';
17
+ import type { SystemAdapters } from './system-adapter';
18
+
19
+ export const NATIVE_INPUT_VALUE_TYPES = [
20
+ 'digital',
21
+ 'scalar',
22
+ 'vector2',
23
+ 'pointerDelta',
24
+ 'pointerPosition',
25
+ ] as const;
26
+ export type NativeInputValueType = (typeof NATIVE_INPUT_VALUE_TYPES)[number];
27
+ export type NativeInputValue = boolean | number | { readonly x: number; readonly y: number };
28
+
29
+ /** A table may be a THUNK, evaluated once at install (after every project
30
+ * module has run), so a game whose declarations accrete during module
31
+ * evaluation — the dev-tools registry's `stat()`/`cheat()` lines beside each
32
+ * mechanic — hands a complete snapshot without ordering its own imports
33
+ * around the debugger. */
34
+ export type NativeTable<T> = Readonly<Record<string, T>> | (() => Readonly<Record<string, T>>);
35
+
36
+ /** A command is a plain function, or `{ description, run }` when it wants the
37
+ * session's `game.commands()` listing to say what it does. Argument
38
+ * validation stays inside the function — the game's words at the game's
39
+ * door. */
40
+ export type NativeCommandEntry =
41
+ | ((...args: unknown[]) => unknown)
42
+ | { readonly description?: string; readonly run: (...args: unknown[]) => unknown };
43
+
44
+ export interface NativeDebugModule {
45
+ readonly commands?: NativeTable<NativeCommandEntry>;
46
+ readonly state?: NativeTable<() => unknown>;
47
+ readonly input?: {
48
+ /** May be a thunk, read LIVE on every use — an app whose action set loads
49
+ * asynchronously (an input map fetched at boot) answers with what it has
50
+ * NOW rather than freezing the empty pre-load set at install. */
51
+ readonly actions:
52
+ | Readonly<Record<string, NativeInputValueType>>
53
+ | (() => Readonly<Record<string, NativeInputValueType>>);
54
+ readonly set: (action: string, value: NativeInputValue) => void;
55
+ readonly clear: () => void;
56
+ /** Optional native-scheduler operation. The adapter never synthesizes one. */
57
+ readonly tap?: (action: string) => void;
58
+ /** Optional native-scheduler operation. The adapter never adds a host phase. */
59
+ readonly scheduleAtTick?: (tick: number, action: string, value: NativeInputValue) => void;
60
+ };
61
+ /** The app's own EVENT STREAM, bridged into the session's tick-stamped
62
+ * event log at install — the entry-export replacement for the react
63
+ * emit hook. The app owns the emitter; the host only forwards. */
64
+ readonly events?: {
65
+ readonly subscribe: (listener: (event: string, detail?: unknown) => void) => () => void;
66
+ };
67
+ /**
68
+ * `false` while the app is intentionally BETWEEN worlds — a scene remount in flight (a
69
+ * `reload_current_scene`-style restart unmounts the running scene and the fresh one arrives
70
+ * on an ASYNC React commit). Session tick drivers (`runTicksWhenSettled`, behind both
71
+ * run-ticks doors) wait for `true` before each tick, so which tick first runs the fresh
72
+ * world is a function of the sim, never of wall timing between driver calls. Omitted =
73
+ * always settled.
74
+ */
75
+ readonly settled?: () => boolean;
76
+ }
77
+
78
+ /** One root's `debug` export, already read and validated, with the root it came from. */
79
+ export interface NativeDebugBinding {
80
+ readonly rootId: string;
81
+ readonly debug: NativeDebugModule;
82
+ }
83
+
84
+ /** One root's statically declared, already-validated system-adapter slots. */
85
+ export interface NativeSystemsBinding {
86
+ readonly rootId: string;
87
+ readonly slots: Readonly<Partial<SystemAdapters>>;
88
+ /**
89
+ * The slots this root's `systems` table answered with `absent(reason)` — the
90
+ * positive absences, in the game's own words.
91
+ *
92
+ * They are carried rather than dropped because they are the ONLY thing that
93
+ * can tell a coverage reader "this game has no networking" apart from "nobody
94
+ * ever looked". Nothing is installed for them (that would be the stub adapter
95
+ * `absent()`'s own comment forbids); they exist to be READ — through
96
+ * `binding.observation.entrySystems`, which is the product door the coverage
97
+ * table renders from.
98
+ */
99
+ readonly absent: readonly ContractSystemEmptySlot[];
100
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The canvas surface's physics seam, keyed by the substrate's display object
3
+ * (`D`, opaque by default). The implementer names its own display type
4
+ * (`@volter/editor-game/runtime/pixi/system-adapters` is `PhysicsAdapter2D<Container>`).
5
+ */
6
+
7
+ /**
8
+ * PhysicsAdapter2D — the Pixi analog of the 3D PhysicsAdapter (rapier-physics-adapter).
9
+ * Lets the editor coordinate editing of a Rapier-2D-driven entity WITHOUT owning the
10
+ * simulation: report transform ownership, then freeze → commit → unfreeze so a drag
11
+ * sticks instead of being stomped by the next physics step.
12
+ *
13
+ * KEYED BY THE DISPLAY OBJECT, and that is this surface's own vocabulary rather
14
+ * than a shortcut: the canvas gizmo's real caller
15
+ * (`editor/src/authoring/pixi-live-write-target.ts`) resolves node id → display
16
+ * itself before it asks, so there is no node-id hop on this lane at all.
17
+ *
18
+ * `keyedBy` is REQUIRED here and optional on the 3D {@link PhysicsAdapter} —
19
+ * see that interface's doc comment for why the union has to be tagged, and why
20
+ * the newcomer is the shape obliged to say so.
21
+ */
22
+ export interface PhysicsAdapter2D<D = unknown> {
23
+ /** Discriminates this shape from the node-id-keyed {@link PhysicsAdapter}
24
+ * inside `SystemAdapters['physics']`. */
25
+ readonly keyedBy: 'display';
26
+ ownerOf(display: D): 'physics' | 'none';
27
+ freeze(display: D): void;
28
+ commit(display: D, position: [number, number], rotation: number): void;
29
+ unfreeze(display: D): void;
30
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The offline-audio seam: what a caller's renderer hands back, and the
3
+ * function shape that produces it. The contract owns both because
4
+ * `SystemAdapters`'s audio door names them; the gameplay export
5
+ * (`editor-game/src/host/gameplay-export.ts`) is what drives a renderer.
6
+ */
7
+
8
+ /** What a caller's renderer hands back: real PCM in a native `AudioBuffer`.
9
+ * `AudioBuffer` — not a volter type — is the point: any offline audio path in
10
+ * the platform already produces one. */
11
+ export interface RenderedAudio {
12
+ readonly buffer: AudioBuffer;
13
+ readonly durationSeconds: number;
14
+ }
15
+
16
+ /** Render `[start, end)` (absolute/canonical seconds) offline and
17
+ * deterministically. The offset trap in the module doc is the renderer's to
18
+ * honour: identical `(start, end)` on an unchanged score must produce
19
+ * identical samples, since the capture driver relies on it. */
20
+ export type OfflineAudioRenderer = (start: number, end: number) => Promise<RenderedAudio>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * What a render-memory reading IS — the shape `SystemAdapters`'s memory door
3
+ * answers with. The contract owns the shape; the measurement that fills it
4
+ * walks a live three.js scene and is the game runtime's
5
+ * (`@volter/editor-game/runtime/dev/render-memory`).
6
+ */
7
+
8
+ export interface RenderMemoryEntry {
9
+ readonly name: string;
10
+ readonly bytes: number;
11
+ }
12
+
13
+ export interface RenderMemoryUnestimated {
14
+ readonly name: string;
15
+ readonly reason: string;
16
+ }
17
+
18
+ export interface RenderMemorySnapshot {
19
+ readonly counts: {
20
+ readonly geometries: number;
21
+ readonly textures: number;
22
+ /** Compiled program count, or `null` when the renderer doesn't expose it
23
+ * (`renderer.info.programs` is populated only after the first render). */
24
+ readonly programs: number | null;
25
+ };
26
+ /** Sum of every ESTIMABLE texture (nulls excluded — they are in
27
+ * `unestimated`). */
28
+ readonly estimatedTextureBytes: number;
29
+ /** Exact sum of geometry attribute + index buffers. */
30
+ readonly estimatedGeometryBytes: number;
31
+ readonly topTextures: readonly RenderMemoryEntry[];
32
+ readonly topGeometries: readonly RenderMemoryEntry[];
33
+ readonly unestimated: readonly RenderMemoryUnestimated[];
34
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `WorldRendererConfig` — what a WORLD may declare about the renderer that
3
+ * draws it, and therefore part of the ROOT CONTRACT: `MountedThreeRoot
4
+ * .rendererConfig` reports it (`./root-adapter.ts`) and
5
+ * `./root-seam-contract.ts` lists it as a contract field.
6
+ *
7
+ * The contract owns the SHAPE and no three.js code: the function that applies
8
+ * it to a live `WebGLRenderer` (and restores what it found on dispose) is the
9
+ * three.js twin's, `@volter/threejs-runtime/adapter/renderer-config`. The host
10
+ * owns the `WebGLRenderer` (`ThreeHostContext.renderer`) and configures it
11
+ * with this engine's defaults: ACES tone mapping, sRGB output, PCF-soft
12
+ * shadows. Those defaults are right for a world authored against them and
13
+ * WRONG for a world authored against a different engine's pipeline — an
14
+ * imported Godot 3 GLES2 game does gamma-space lighting with no tonemapper at
15
+ * all, so ACES quietly desaturates and darkens every colour its author picked.
16
+ *
17
+ * Three properties of the shape are load-bearing:
18
+ *
19
+ * - **It is per-renderer, never engine-wide.** Every field is a
20
+ * `WebGLRenderer` instance property, and a play root gets its own renderer.
21
+ * Nothing here reaches a module-level three global, so one world's
22
+ * declaration cannot change how the editor's own viewport, another root, or
23
+ * a thumbnail bake renders.
24
+ * - **Absent means "leave the host's value alone".** Every field is optional
25
+ * and an omitted one is never written, so declaring a tone mapping does not
26
+ * silently reset the clear colour.
27
+ * - **It is restored on dispose.** The renderer outlives the mount, so a
28
+ * world that did not put back what it found would leak its pipeline into
29
+ * whatever mounts next.
30
+ *
31
+ * This is deliberately NOT a general render-settings system. It carries what a
32
+ * world can honestly state about its own colour pipeline and nothing else; a
33
+ * property the host fixes at CONSTRUCTION (the WebGL context's `antialias`
34
+ * attribute, and therefore the MSAA sample count) cannot be declared here,
35
+ * because there would be no honest moment to apply it.
36
+ */
37
+
38
+ /** The tone-mapping operators three exposes, named as data rather than as three's numeric enum. */
39
+ export type WorldToneMapping =
40
+ | 'none'
41
+ | 'linear'
42
+ | 'reinhard'
43
+ | 'cineon'
44
+ | 'aces'
45
+ | 'agx'
46
+ | 'neutral';
47
+
48
+ /**
49
+ * The output transfer function the frame is written with.
50
+ *
51
+ * - `srgb` — three's own default and this engine's: linear lighting, sRGB encode on output.
52
+ * - `srgb-linear` — NO output transform. This is what a gamma-space renderer needs: the shading
53
+ * result is already in display space and encoding it a second time washes the frame out.
54
+ */
55
+ export type WorldOutputColorSpace = 'srgb' | 'srgb-linear';
56
+
57
+ /**
58
+ * The shadow-map filter, named as data rather than as three's numeric enum.
59
+ *
60
+ * This is a renderer INSTANCE property (`WebGLRenderer.shadowMap.type`), not a context attribute,
61
+ * so unlike MSAA it has an honest moment at which a world can ask for it — which is the whole test
62
+ * this file's header states. A source engine that declares its own shadow filter (Godot 3's
63
+ * `rendering/quality/shadows/filter_mode`) would otherwise inherit whatever the host built with.
64
+ */
65
+ export type WorldShadowMapType = 'basic' | 'pcf' | 'pcf-soft' | 'vsm';
66
+
67
+ /** What a world may declare about the renderer that draws it. Every field is optional; see header. */
68
+ export interface WorldRendererConfig {
69
+ readonly toneMapping?: WorldToneMapping | undefined;
70
+ readonly toneMappingExposure?: number | undefined;
71
+ readonly outputColorSpace?: WorldOutputColorSpace | undefined;
72
+ /**
73
+ * `WebGLRenderer.shadowMap.type`. Writing it after a shadow map has already been built needs
74
+ * `shadowMap.needsUpdate`, which this function sets — three caches the compiled depth material
75
+ * per type and would otherwise keep filtering with the previous one.
76
+ */
77
+ readonly shadowMapType?: WorldShadowMapType | undefined;
78
+ /**
79
+ * The colour the frame is cleared to, as a CSS hex string. The renderer's existing clear ALPHA
80
+ * is preserved: a stacked canvas is transparent on purpose (`create-runtime.ts` gives every
81
+ * non-bottom root `alpha: true`), and forcing it opaque here would hide every layer below.
82
+ */
83
+ readonly clearColor?: string | undefined;
84
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * RootAdapter — the host ⇄ runtime contract. A ROOT is *anything that mounts*.
3
+ *
4
+ * This is the interface the host depends on. First-party content and
5
+ * unmodified external games are peer root adapters. A game is the
6
+ * manifest's complete roots array, while each adapter owns exactly one
7
+ * root.
8
+ */
9
+
10
+ import type { AdapterSurface } from './adapter-surface';
11
+ import type { AuthoringAdapter } from './authoring';
12
+ import type { HostContextFor } from './host-context';
13
+ import type { WorldRendererConfig } from './renderer-config';
14
+ import type { SystemAdapters } from './system-adapter';
15
+
16
+ /**
17
+ * The ingested-world observation contract (T7.4 slice 2, "the
18
+ * must-answer"). Cross-world data flow with a
19
+ * foreign game is observation via an adapter-provided interface, not ordinary
20
+ * shared state (that's D7's answer for first-party↔first-party flow).
21
+ * Optional on {@link MountedThreeRoot} —
22
+ * absence means the adapter has nothing genuinely observable to expose (the
23
+ * anti-shim rule: never fabricate state), and the world takes the honest
24
+ * "no state bridge" tier hit (see `Game.registerRoot` in `runtime/game.ts`)
25
+ * rather than silently offering an `undefined`-forever subscription.
26
+ */
27
+ export interface RootStateObserver {
28
+ /**
29
+ * Notified at most once per frame IF the adapter can hook the game's own
30
+ * update (`loop: 'gated'` roots, §(d)); self-driven roots (`loop:
31
+ * 'self-driven'`, raw-rAF) may notify on their OWN rAF cadence instead —
32
+ * consumers must not assume our frame timing. Returns an unsubscribe
33
+ * function.
34
+ */
35
+ subscribe(onChange: () => void): () => void;
36
+ /**
37
+ * A cheap, adapter-chosen snapshot of this world's observable state.
38
+ * Returns a STABLE reference while nothing has changed (so a selector run
39
+ * over it can cheaply bail out by reference, same spirit as
40
+ * `GameStateBridge`'s frame-version cache). Shape is adapter-defined —
41
+ * not centrally schematized in v1.
42
+ */
43
+ snapshot(): unknown;
44
+ }
45
+
46
+ /**
47
+ * Everything a mounted world provides except its render surface. Tagged
48
+ * mounted-root variants carry their native surface fields: a Three scene and
49
+ * camera, a declared canvas substrate root, or a DOM container.
50
+ */
51
+ export interface MountedRootBase {
52
+ /**
53
+ * Loop model:
54
+ * - `false` (host-driven): first-party + "clean" externals. The host ticks
55
+ * `update(dt)` in its loop.
56
+ * - `true` (self-driven): an unmodified game that owns its renderer + rAF.
57
+ * The host does NOT tick it. Pause/step control is a DECLARED CAPABILITY,
58
+ * not a promise: implement `setPaused`/`step` only where the game exposes
59
+ * a sanctioned pause/step mechanism. Gating a raw-rAF loop from outside
60
+ * was demonstrated and REJECTED (D5 — it halts the loop rather than
61
+ * pausing it). Where `setPaused` is absent the host reports the
62
+ * shortfall loudly, naming the mechanism that would close it (T7.6).
63
+ */
64
+ readonly drivesOwnLoop: boolean;
65
+
66
+ /**
67
+ * Which host clock owns an opaque mounted root's single `update(dt)` hook.
68
+ *
69
+ * `fixed` (the default) preserves the adapter seam's original contract:
70
+ * the callback runs once per consumed simulation substep. `display` is for
71
+ * a reconciler whose one native advance is inseparably BOTH its callbacks
72
+ * and its draw (React Three Fiber's `advance()` is the measured case). The
73
+ * real host then withholds it from fixed catch-up substeps and invokes it
74
+ * exactly once from the display pass. A direct/offline `runFrame` that does
75
+ * not skip render phases still invokes it once, preserving capture and
76
+ * explicit-tick semantics.
77
+ *
78
+ * Absent means `fixed`. This changes no foreign/ingest adapter unless that
79
+ * adapter opts in explicitly.
80
+ */
81
+ readonly updateCadence?: 'fixed' | 'display';
82
+
83
+ update?(dt: number): void; // host-driven only
84
+ fixedUpdate?(dt: number): void;
85
+ setPaused?(paused: boolean): void; // capability, not promise — see loop-model note above
86
+ step?(): void;
87
+
88
+ resize?(width: number, height: number): void;
89
+ /** Does this world claim a pointer at (x, y), in CSS pixels from its surface's top-left? The input
90
+ * router asks it of a world stacked above the bottom one (a 2D layer's interactive Pixi objects).
91
+ * Absent: such a world claims no point, and input falls through to the bottom world. */
92
+ hitTest?(x: number, y: number): boolean;
93
+ dispose(): void;
94
+ /** Resolves after an asynchronous native reconciler has run every component
95
+ * cleanup started by {@link dispose}. Omitted by synchronously-disposing
96
+ * roots. Hosts use this before auditing/reclaiming realm-owned resources. */
97
+ readonly disposeComplete?: Promise<void>;
98
+
99
+ /** Optional capability providers — absence = "not supported", host degrades. */
100
+ readonly authoring?: AuthoringAdapter;
101
+ readonly systems?: SystemAdapters;
102
+ /** Adapter-owned evaluation identity used to bind static system factories. */
103
+ readonly systemScope?: object;
104
+ /** Optional state-observation capability (T7.4 slice 2, §4) — absent means
105
+ * "no state bridge"; `Game.registerRoot` reports this loudly, once, for
106
+ * self-driven mount. A host-driven mount ticks inside the game's frame,
107
+ * where the editor reads it directly, so it needs none. */
108
+ readonly observe?: RootStateObserver;
109
+ }
110
+
111
+ /** A live, mounted Three world. The host obtains `scene`/`camera` to render
112
+ * and author it; they are opaque here, and `@volter/editor-threejs` names the
113
+ * Three-typed root. */
114
+ export interface MountedThreeRoot<Scene = unknown, Camera = unknown> extends MountedRootBase {
115
+ readonly kind: 'three';
116
+ /** The live scene + camera the editor inspects/renders for authoring. */
117
+ readonly scene: Scene;
118
+ readonly camera: Camera;
119
+ /**
120
+ * The colour pipeline this world was authored for, REPORTED rather than
121
+ * applied — the adapter has already applied it to the renderer its own host
122
+ * handed it (`./renderer-config.ts`).
123
+ *
124
+ * It is here because the host that MOUNTS a world is not always the host
125
+ * that DRAWS it. The editor's design session mounts against a
126
+ * non-rasterizing renderer on purpose and then draws the mounted scene with
127
+ * the editor viewport's own `WebGLRenderer`, so the world's declaration
128
+ * lands on a surface with no pixels while the surface with pixels never
129
+ * hears it. Reporting the declaration is what lets that host apply the same
130
+ * config, through the same `applyWorldRendererConfig`, to the renderer that
131
+ * actually produces the frame.
132
+ *
133
+ * Absent means the world declared nothing and the drawing host should leave
134
+ * its own configuration alone — the same "absent = don't touch" rule the
135
+ * config's own fields follow.
136
+ */
137
+ readonly rendererConfig?: WorldRendererConfig | undefined;
138
+ }
139
+
140
+ /**
141
+ * A canvas substrate's native root, carried without teaching the universal
142
+ * host that substrate's API. The declaration is explicit — editor substrate
143
+ * adapters dispatch on `name`; they never infer a library from object shape.
144
+ */
145
+ export interface MountedCanvasSubstrate<T = unknown> {
146
+ readonly name: string;
147
+ readonly root: T;
148
+ /** The substrate's own module namespace when authoring needs constructors
149
+ * (for example Babylon's native GizmoManager). Omitted when the projection
150
+ * only needs the root itself, as Pixi's does. */
151
+ readonly api?: unknown;
152
+ }
153
+
154
+ /** A live, mounted canvas world. The host owns the canvas and lifecycle; the
155
+ * mounted root states which native substrate inhabits it and hands that
156
+ * substrate's root through opaquely. */
157
+ export interface MountedCanvasRoot extends MountedRootBase {
158
+ readonly kind: 'canvas';
159
+ /** The same host-owned canvas passed to `mount`. */
160
+ readonly canvas: HTMLCanvasElement;
161
+ readonly substrate: MountedCanvasSubstrate;
162
+ }
163
+
164
+ /** A live, mounted react world (T6.2) — the react analog of
165
+ * {@link MountedThreeRoot}. `container` is the DOM-root layer the host
166
+ * handed the adapter's `mount` (the SAME element `RootInstance.reactRoot()`
167
+ * returns) — a react world's tree renders into it via `createRoot`. */
168
+ export interface MountedReactRoot extends MountedRootBase {
169
+ readonly kind: 'dom';
170
+ readonly container: HTMLElement;
171
+ }
172
+
173
+ /** Every kind of live, mounted world. */
174
+ export type MountedRoot = MountedThreeRoot | MountedCanvasRoot | MountedReactRoot;
175
+
176
+ /** Map a {@link AdapterSurface} to its mounted-world shape — lets generic
177
+ * code over
178
+ * `K extends AdapterSurface` name the right surface without a manual union. */
179
+ export type MountedRootFor<K extends AdapterSurface> = K extends 'three'
180
+ ? MountedThreeRoot
181
+ : K extends 'canvas'
182
+ ? MountedCanvasRoot
183
+ : K extends 'dom'
184
+ ? MountedReactRoot
185
+ : never;
186
+
187
+ /**
188
+ * The interface every root implements to run on the host. Generic over
189
+ * {@link AdapterSurface} so `mount` receives and returns its native surface
190
+ * types. Bare `RootAdapter` denotes the first-party Three surface.
191
+ *
192
+ * `K` parameterizes both halves of `mount`: `HostContextFor<K>` selects the
193
+ * host context and `MountedRootFor<K>` selects the mounted result. Production
194
+ * adapters are genuine `RootAdapter<K>` subtypes —
195
+ * `Pixi2DRootAdapter extends RootAdapter<'canvas'>`, `ReactRootAdapter extends
196
+ * RootAdapter<'dom'>` (`runtime/create-runtime.ts`).
197
+ */
198
+ /**
199
+ * The `= 'three'` default is type-level only: an author who writes bare
200
+ * `RootAdapter` for
201
+ * a pixi root gets a compile error on `mount`'s return type, because
202
+ * `MountedRootFor<'three'>` demands `scene`/`camera` a `MountedCanvasRoot` has
203
+ * not got.
204
+ */
205
+ export interface RootAdapter<K extends AdapterSurface = 'three'> {
206
+ /** Stable id (telemetry/registry/conformance). */
207
+ readonly id: string;
208
+ /** Build/start the game against the host context for ITS surface; return the
209
+ * handle. */
210
+ mount(host: HostContextFor<K>): Promise<MountedRootFor<K>>;
211
+ }
212
+
213
+ /**
214
+ * One surface tag paired with an adapter typed FOR that surface.
215
+ *
216
+ * A resolver that turns a manifest root into a mountable
217
+ * adapter handles every surface in one function, so its return type has to
218
+ * span all three. Returning the bare union `RootAdapter<'three'> |
219
+ * RootAdapter<'canvas'> | RootAdapter<'dom'>` does NOT work — a union of
220
+ * function types INTERSECTS its parameters, so `.mount(host)` becomes
221
+ * uncallable with any single host value (no one object is simultaneously a
222
+ * `ThreeHostContext`, a `CanvasHostContext` and a `DomHostContext`).
223
+ *
224
+ * Carrying the tag ALONGSIDE the adapter fixes that structurally: this is a
225
+ * discriminated union on `surface`, so `switch (resolved.surface)` narrows
226
+ * `resolved.adapter` to ONE `RootAdapter<K>` inside each branch — a single
227
+ * signature again, hence `.mount(host)` is callable with that surface's own
228
+ * host context, checked by the compiler instead of asserted.
229
+ */
230
+ export type SurfaceAdapterFor<K extends AdapterSurface> = {
231
+ readonly surface: K;
232
+ readonly adapter: RootAdapter<K>;
233
+ };
234
+
235
+ /** Every surface's {@link SurfaceAdapterFor}, as one discriminated union.
236
+ * Written out member-by-member (rather than as a distributive conditional)
237
+ * so `assertNever` sees exactly three variants and a hypothetical 4th
238
+ * `AdapterSurface` member fails to compile HERE, at the vocabulary, rather
239
+ * than silently widening every consumer's exhaustiveness guard. */
240
+ export type SurfaceAdapter =
241
+ | SurfaceAdapterFor<'three'>
242
+ | SurfaceAdapterFor<'canvas'>
243
+ | SurfaceAdapterFor<'dom'>;