@volter/project 0.0.0-stage → 0.5.203
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/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +18 -3
- package/package.json +64 -4
- package/schemas/engine-api.json +57 -0
- package/schemas/engine-api.md +22 -0
- package/schemas/volter-project.schema.json +468 -0
- package/schemas/volter-settings.schema.json +149 -0
- package/src/adapter/adapter-module.ts +997 -0
- package/src/adapter/adapter-surface.ts +34 -0
- package/src/adapter/authoring-seam-contract.ts +224 -0
- package/src/adapter/authoring.ts +1336 -0
- package/src/adapter/binding.ts +445 -0
- package/src/adapter/debug-registry-ref.ts +19 -0
- package/src/adapter/editor-looks.ts +519 -0
- package/src/adapter/finders/finder-result.ts +29 -0
- package/src/adapter/finders/registry.ts +116 -0
- package/src/adapter/frame-capture.ts +80 -0
- package/src/adapter/host-context.ts +114 -0
- package/src/adapter/index.ts +209 -0
- package/src/adapter/ingest/contract-system-slots.ts +28 -0
- package/src/adapter/ingest/game-contract-seams.ts +86 -0
- package/src/adapter/ingest/game-contract.ts +506 -0
- package/src/adapter/ingest/mount-readiness.ts +140 -0
- package/src/adapter/loop-gate-report.ts +53 -0
- package/src/adapter/manifest-interpreter.ts +64 -0
- package/src/adapter/native-entry-surface.ts +100 -0
- package/src/adapter/physics-adapter-2d.ts +30 -0
- package/src/adapter/render-audio.ts +20 -0
- package/src/adapter/render-memory.ts +34 -0
- package/src/adapter/renderer-config.ts +84 -0
- package/src/adapter/root-adapter.ts +243 -0
- package/src/adapter/root-seam-contract.ts +61 -0
- package/src/adapter/root-stacking.ts +67 -0
- package/src/adapter/seam-evidence.ts +336 -0
- package/src/adapter/system-adapter.ts +865 -0
- package/src/adapter/system-seam-contract.ts +138 -0
- package/src/adapter/system-slot.ts +301 -0
- package/src/adapter/transform.ts +35 -0
- package/src/adapter/workspace-arrangement.ts +92 -0
- package/src/core/system-phase.ts +52 -0
- package/src/inspection.ts +281 -0
- package/src/manifest/configuration-kinds.ts +241 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +45 -0
- package/src/manifest/kind-modules.ts +58 -0
- package/src/manifest/load-file.ts +33 -0
- package/src/manifest/load.ts +380 -0
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/runtime-environment.ts +48 -0
- package/src/manifest/schema.ts +690 -0
- package/src/settings/keys.ts +152 -0
- 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'>;
|