@volter/project 0.5.210 → 0.5.212
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/NOTICE +2 -2
- package/README.md +8 -15
- package/package.json +5 -26
- package/schemas/volter-project.schema.json +33 -145
- package/schemas/volter-settings.schema.json +11 -49
- package/src/adapter/adapter-module.ts +204 -465
- package/src/adapter/adapter-surface.ts +4 -32
- package/src/adapter/authoring-seam-contract.ts +7 -24
- package/src/adapter/authoring.ts +126 -265
- package/src/adapter/editor-looks.ts +96 -127
- package/src/adapter/finders/registry.ts +6 -21
- package/src/adapter/host-context.ts +17 -72
- package/src/adapter/index.ts +4 -77
- package/src/adapter/manifest-interpreter.ts +7 -50
- package/src/adapter/physics-adapter-2d.ts +4 -21
- package/src/adapter/root-adapter.ts +37 -111
- package/src/adapter/root-seam-contract.ts +3 -6
- package/src/adapter/root-stacking.ts +4 -46
- package/src/adapter/seam-evidence.ts +0 -16
- package/src/adapter/system-adapter.ts +24 -656
- package/src/adapter/system-seam-contract.ts +1 -91
- package/src/adapter/transform.ts +4 -3
- package/src/adapter/workspace-arrangement.ts +29 -49
- package/src/manifest/editor-port.ts +10 -36
- package/src/manifest/filename.ts +3 -42
- package/src/manifest/load-file.ts +9 -15
- package/src/manifest/load.ts +44 -179
- package/src/manifest/locate.ts +6 -29
- package/src/manifest/schema.ts +155 -427
- package/src/semver.ts +16 -0
- package/src/settings/keys.ts +9 -11
- package/src/settings/schema.ts +55 -44
- package/schemas/engine-api.json +0 -57
- package/schemas/engine-api.md +0 -22
- package/src/adapter/binding.ts +0 -445
- package/src/adapter/debug-registry-ref.ts +0 -19
- package/src/adapter/frame-capture.ts +0 -80
- package/src/adapter/ingest/contract-system-slots.ts +0 -28
- package/src/adapter/ingest/game-contract-seams.ts +0 -86
- package/src/adapter/ingest/game-contract.ts +0 -506
- package/src/adapter/ingest/mount-readiness.ts +0 -140
- package/src/adapter/loop-gate-report.ts +0 -53
- package/src/adapter/native-entry-surface.ts +0 -100
- package/src/adapter/render-audio.ts +0 -20
- package/src/adapter/render-memory.ts +0 -34
- package/src/adapter/renderer-config.ts +0 -84
- package/src/adapter/system-slot.ts +0 -301
- package/src/core/system-phase.ts +0 -52
- package/src/inspection.ts +0 -281
- package/src/manifest/configuration-kinds.ts +0 -241
- package/src/manifest/kind-modules.ts +0 -58
- package/src/manifest/runtime-environment.ts +0 -48
|
@@ -1,53 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
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>;
|
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `createSystemSlot` — THE implementation of the declared-slot façade, the
|
|
3
|
-
* seam between a STATIC `systems` declaration and a LIVE adapter that only
|
|
4
|
-
* exists once the world tree is mounted.
|
|
5
|
-
*
|
|
6
|
-
* WHY IT EXISTS. An entry declares its systems statically
|
|
7
|
-
* (`export const systems = { networking: networkingSystem() }`), while the
|
|
8
|
-
* thing being declared — a joined Colyseus room, a baked navmesh, a mounted
|
|
9
|
-
* `<Physics>`, a cutscene's camera ownership — is built inside the world tree,
|
|
10
|
-
* later, and may be replaced (reconnect, remount, hot restart) or go away
|
|
11
|
-
* (stop). The slot is a stable object the host installs at mount and holds
|
|
12
|
-
* forever; every call forwards to whatever is attached right now.
|
|
13
|
-
*
|
|
14
|
-
* ── THE HONESTY RULE, STATED IN ONE PLACE ───────────────────────────────────
|
|
15
|
-
* This is why the pattern is centralized: five hand-rolled copies disagreed
|
|
16
|
-
* about it, and each disagreement was found by a separate review.
|
|
17
|
-
*
|
|
18
|
-
* - An **action** with no live target REFUSES BY NAME
|
|
19
|
-
* (`<name>: <unattachedMessage>`). Never a silent no-op — a caller that
|
|
20
|
-
* told the game to DO something must learn that it did not happen. An
|
|
21
|
-
* action against a live adapter that lacks the member refuses the same
|
|
22
|
-
* way, naming the member.
|
|
23
|
-
* - A **read** answers its declared HONEST EMPTY — `'disconnected'`, `[]`,
|
|
24
|
-
* `null`, `false`. That is not a fabrication: it is the truth of the
|
|
25
|
-
* not-attached state, in the same vocabulary the editor's own edit-mode
|
|
26
|
-
* adapters use. Reads are also the only kind safe to call from a per-frame
|
|
27
|
-
* debug draw, which is why a throw into a rAF loop is never the answer.
|
|
28
|
-
* - An **optional** member MIRRORS THE LIVE ADAPTER'S PRESENCE: absent while
|
|
29
|
-
* unattached, and absent afterwards when the live adapter itself does not
|
|
30
|
-
* provide it. Presence IS the consume-by-presence protocol's signal, so
|
|
31
|
-
* the slot may never invent it. Each presence read CAPTURES the adapter it
|
|
32
|
-
* saw and forwards to that same instance, so a detach between the read and
|
|
33
|
-
* the call answers as the adapter the caller was handed rather than a
|
|
34
|
-
* mid-flight swap.
|
|
35
|
-
* - A **subscribe** member SURVIVES REATTACHMENT: listeners live on the
|
|
36
|
-
* slot, never on the instance, and the slot re-binds its own single
|
|
37
|
-
* subscription on every attach/detach. A panel that subscribed once to the
|
|
38
|
-
* stable slot object is therefore never left holding a disposed system, and
|
|
39
|
-
* it is notified when the live half appears or disappears.
|
|
40
|
-
* - A member the config DOES NOT LIST does not exist on the slot at all.
|
|
41
|
-
* That is how the anti-shim rule spells omission — e.g. `PhysicsAdapter`'s
|
|
42
|
-
* `debugDraw`/`contactPoints` over `@react-three/rapier`, whose debug line
|
|
43
|
-
* segments live inside the fiber tree and expose no handle. Absence is a
|
|
44
|
-
* capability answer; a stub is a lie.
|
|
45
|
-
*
|
|
46
|
-
* ── RESOURCE OWNERSHIP ──────────────────────────────────────────────────────
|
|
47
|
-
* OWNER: whoever built the live adapter (the hook, the bridge component). The
|
|
48
|
-
* slot owns only the POINTER to it plus its own listener set and its own
|
|
49
|
-
* subscription to the live half. SHARERS: every reader of the declared
|
|
50
|
-
* system — editor inspectors, instruments. TEARDOWN: the function `attach`
|
|
51
|
-
* returns, and nothing else. It is IDENTITY-GUARDED — it clears only while
|
|
52
|
-
* the same instance is still attached, so a replace-then-teardown-the-old
|
|
53
|
-
* ordering cannot detach the new one — and detaching never disposes the
|
|
54
|
-
* adapter, because the slot did not create it.
|
|
55
|
-
*
|
|
56
|
-
* Static declarations shared across mounts must use `declareScopedSystem`.
|
|
57
|
-
* The adapter supplies an opaque mount scope and the host resolves the declaration
|
|
58
|
-
* once per root. A slot then belongs to that evaluation, never to the page.
|
|
59
|
-
*
|
|
60
|
-
* NOT A WRAPPER. The slot's members have the adapter interface's own exact
|
|
61
|
-
* signatures and hand back the implementation's own values; nothing stands
|
|
62
|
-
* between a caller and the live library object.
|
|
63
|
-
*/
|
|
64
|
-
|
|
65
|
-
import type { VolterGameSystemEmpty } from './ingest/game-contract';
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* `absent(reason)` — THE WHOLE-SLOT counterpart of the four per-member policies
|
|
69
|
-
* above, and the fifth line of the same honesty rule.
|
|
70
|
-
*
|
|
71
|
-
* The four policies answer "this slot exists but nothing is attached yet". This
|
|
72
|
-
* one answers a different question entirely: **this game will never have that
|
|
73
|
-
* subsystem, and here is what was searched.** A slot simply left out of the
|
|
74
|
-
* `systems` table cannot say that — an omitted key means "unsupported", which is
|
|
75
|
-
* the same shape as "nobody looked", and the coverage report has to file it as a
|
|
76
|
-
* work order forever. Declaring the absence turns an UNANSWERED row into a
|
|
77
|
-
* terminal one.
|
|
78
|
-
*
|
|
79
|
-
* ```ts
|
|
80
|
-
* export const systems = {
|
|
81
|
-
* navigation: createNavigationAdapter(navigation),
|
|
82
|
-
* networking: absent('single-player: no Colyseus client anywhere in `src/`'),
|
|
83
|
-
* };
|
|
84
|
-
* ```
|
|
85
|
-
*
|
|
86
|
-
* ## It is a CONSTRUCTOR for the shape that already exists, not a second one
|
|
87
|
-
*
|
|
88
|
-
* The returned value IS `VolterGameSystemEmpty` — the plain
|
|
89
|
-
* `{ present: false, evidence }` record an INGESTED game declares on
|
|
90
|
-
* `window.volterGame.systems.systemAdapters`, validated by the same single
|
|
91
|
-
* projection (`ingest/contract-system-adapters.ts`). There is one shape law, not
|
|
92
|
-
* two: a foreign game cannot import this helper and must stay able to write the
|
|
93
|
-
* record by hand, so nothing here may brand the runtime value. What the helper
|
|
94
|
-
* buys a FIRST-PARTY author is the reason-quality check moved to the call site —
|
|
95
|
-
* our own code failing its own contract fails fast, where an ingested game's
|
|
96
|
-
* same mistake is filed as a `malformed` verdict for its coverage report.
|
|
97
|
-
*
|
|
98
|
-
* ## It is a MARKER, never a stub adapter
|
|
99
|
-
*
|
|
100
|
-
* Do not answer an absent slot with an adapter whose methods return zeros. A
|
|
101
|
-
* `getConnectionState()` of `'disconnected'` on a game with no transport implies
|
|
102
|
-
* a connection that could exist; that is the fabrication the anti-shim rule
|
|
103
|
-
* forbids. The absence is STATED, and the host installs nothing for it.
|
|
104
|
-
*
|
|
105
|
-
* @param reason What was searched and what was found, in the game's own source
|
|
106
|
-
* terms. "no netcode anywhere in `src/`" is a finished answer a reviewer can
|
|
107
|
-
* re-run; "not implemented yet" is a plan, and is rejected here.
|
|
108
|
-
*/
|
|
109
|
-
export function absent(reason: string): VolterGameSystemEmpty {
|
|
110
|
-
if (typeof reason !== 'string' || reason.trim() === '') {
|
|
111
|
-
throw new Error(
|
|
112
|
-
'absent(): an absence needs evidence — say what was searched and what was found ' +
|
|
113
|
-
'(e.g. "no Colyseus client anywhere in `src/`"), so a reviewer can re-run it. ' +
|
|
114
|
-
'An unevidenced absence asserts something nobody can re-check.',
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
return { present: false, evidence: reason };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Any adapter member. `never[]` args make the conditional match contravariantly. */
|
|
121
|
-
type AnyFn = (...args: never[]) => unknown;
|
|
122
|
-
|
|
123
|
-
/** The declared member's return type, used to type a `read`'s honest empty. */
|
|
124
|
-
type MemberReturn<F> = NonNullable<F> extends (...args: never[]) => infer R ? R : never;
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* The per-member policy. There are exactly four, and every member of every
|
|
128
|
-
* declared slot in the repo is one of them — see the honesty rule above.
|
|
129
|
-
*/
|
|
130
|
-
export type SystemSlotMemberSpec<F> =
|
|
131
|
-
| { readonly kind: 'action' }
|
|
132
|
-
| { readonly kind: 'read'; readonly empty: MemberReturn<F> }
|
|
133
|
-
| { readonly kind: 'subscribe' }
|
|
134
|
-
| { readonly kind: 'optional' };
|
|
135
|
-
|
|
136
|
-
/** The keys of `T` whose values are callable (data properties are not slottable). */
|
|
137
|
-
type FunctionMemberKeys<T> = {
|
|
138
|
-
[K in keyof T]-?: NonNullable<T[K]> extends AnyFn ? K : never;
|
|
139
|
-
}[keyof T];
|
|
140
|
-
|
|
141
|
-
/** Per-member policies. A member left out is left OFF the slot entirely. */
|
|
142
|
-
export type SystemSlotMembers<T> = {
|
|
143
|
-
readonly [K in FunctionMemberKeys<T>]?: SystemSlotMemberSpec<T[K]>;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
export interface SystemSlotConfig<T extends object> {
|
|
147
|
-
/**
|
|
148
|
-
* The slot's own name, used as the prefix of every refusal so a stack-less
|
|
149
|
-
* error still names the seam (`'networkingSystem'`).
|
|
150
|
-
*/
|
|
151
|
-
readonly name: string;
|
|
152
|
-
/**
|
|
153
|
-
* The rest of an action's refusal while nothing is attached — the DOMAIN
|
|
154
|
-
* half of the message, naming what has not happened yet ("the world is not
|
|
155
|
-
* playing, or the navmesh has not finished building").
|
|
156
|
-
*/
|
|
157
|
-
readonly unattachedMessage: string;
|
|
158
|
-
readonly members: SystemSlotMembers<T>;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export interface SystemSlot<T extends object> {
|
|
162
|
-
/**
|
|
163
|
-
* Publish `live` as the slot's target. Returns the ONE detach path, which
|
|
164
|
-
* is identity-guarded: it clears only while `live` is still the attached
|
|
165
|
-
* instance, and never disposes it.
|
|
166
|
-
*/
|
|
167
|
-
attach(live: T): () => void;
|
|
168
|
-
/** The stable declared object. Hand THIS to the entry's `systems` map. */
|
|
169
|
-
readonly slot: T;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function createSystemSlot<T extends object>(config: SystemSlotConfig<T>): SystemSlot<T> {
|
|
173
|
-
const { name, unattachedMessage, members } = config;
|
|
174
|
-
|
|
175
|
-
let live: T | null = null;
|
|
176
|
-
/** Subscribers to the stable slot object; they outlive any one live instance. */
|
|
177
|
-
const listeners = new Set<() => void>();
|
|
178
|
-
/** The slot's own single subscription to whatever is attached. */
|
|
179
|
-
let liveUnsubscribe: (() => void) | null = null;
|
|
180
|
-
let subscribeMember: string | null = null;
|
|
181
|
-
|
|
182
|
-
const asRecord = (value: object): Record<string, unknown> =>
|
|
183
|
-
value as unknown as Record<string, unknown>;
|
|
184
|
-
|
|
185
|
-
const liveMember = (member: string): AnyFn | undefined => {
|
|
186
|
-
if (!live) return undefined;
|
|
187
|
-
const value = asRecord(live)[member];
|
|
188
|
-
return typeof value === 'function' ? (value as AnyFn) : undefined;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const notify = (): void => {
|
|
192
|
-
for (const listener of [...listeners]) listener();
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const bindLive = (): void => {
|
|
196
|
-
liveUnsubscribe?.();
|
|
197
|
-
liveUnsubscribe = null;
|
|
198
|
-
if (!live || subscribeMember === null || listeners.size === 0) return;
|
|
199
|
-
const subscribe = liveMember(subscribeMember);
|
|
200
|
-
if (!subscribe) return;
|
|
201
|
-
const result = subscribe.call(live, notify as never);
|
|
202
|
-
liveUnsubscribe = typeof result === 'function' ? (result as () => void) : null;
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
const target: Record<string, unknown> = {};
|
|
206
|
-
|
|
207
|
-
const declared = Object.entries(members) as [string, SystemSlotMemberSpec<unknown>][];
|
|
208
|
-
for (const [member, spec] of declared) {
|
|
209
|
-
switch (spec.kind) {
|
|
210
|
-
case 'action': {
|
|
211
|
-
// Refuse by name, twice over: no live adapter at all, or a live
|
|
212
|
-
// adapter that does not carry this member (only ever a foreign
|
|
213
|
-
// replacement — the blessed implementations carry all of theirs).
|
|
214
|
-
target[member] = (...args: never[]): unknown => {
|
|
215
|
-
if (!live) throw new Error(`${name}: ${unattachedMessage}`);
|
|
216
|
-
const fn = liveMember(member);
|
|
217
|
-
if (!fn) throw new Error(`${name}: the live adapter has no ${member}().`);
|
|
218
|
-
return fn.apply(live, args);
|
|
219
|
-
};
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
case 'read': {
|
|
223
|
-
const { empty } = spec;
|
|
224
|
-
target[member] = (...args: never[]): unknown => {
|
|
225
|
-
const current = live;
|
|
226
|
-
const fn = liveMember(member);
|
|
227
|
-
const value = fn ? fn.apply(current, args) : undefined;
|
|
228
|
-
return value ?? empty;
|
|
229
|
-
};
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
case 'subscribe': {
|
|
233
|
-
if (subscribeMember !== null) {
|
|
234
|
-
throw new Error(
|
|
235
|
-
`${name}: a slot declares at most one 'subscribe' member (${subscribeMember} and ${member}).`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
subscribeMember = member;
|
|
239
|
-
target[member] = (listener: () => void): (() => void) => {
|
|
240
|
-
listeners.add(listener);
|
|
241
|
-
bindLive();
|
|
242
|
-
return () => {
|
|
243
|
-
listeners.delete(listener);
|
|
244
|
-
bindLive();
|
|
245
|
-
};
|
|
246
|
-
};
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
case 'optional': {
|
|
250
|
-
Object.defineProperty(target, member, {
|
|
251
|
-
enumerable: true,
|
|
252
|
-
configurable: true,
|
|
253
|
-
get(): AnyFn | undefined {
|
|
254
|
-
const current = live;
|
|
255
|
-
const fn = liveMember(member);
|
|
256
|
-
if (!fn) return undefined;
|
|
257
|
-
return (...args: never[]): unknown => fn.apply(current, args);
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const attach = (next: T): (() => void) => {
|
|
266
|
-
live = next;
|
|
267
|
-
bindLive();
|
|
268
|
-
notify();
|
|
269
|
-
return () => {
|
|
270
|
-
if (live !== next) return;
|
|
271
|
-
live = null;
|
|
272
|
-
bindLive();
|
|
273
|
-
notify();
|
|
274
|
-
};
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
// THE ONE CAST. `exactOptionalPropertyTypes` forbids an optional member
|
|
278
|
-
// whose getter can yield `undefined`, but that is precisely the honest
|
|
279
|
-
// shape here — the getter IS how presence mirrors the live adapter — and a
|
|
280
|
-
// reflectively built object cannot be typed structurally anyway. Callers
|
|
281
|
-
// see exactly the absent/present behavior the adapter interface documents.
|
|
282
|
-
return { attach, slot: target as T };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/** Optional mount binding for a static system declaration. The contract knows
|
|
286
|
-
* only an opaque identity; the implementing adapter owns its native scope. */
|
|
287
|
-
const scopedSystems = new WeakMap<object, (scope: object) => object>();
|
|
288
|
-
|
|
289
|
-
export function declareScopedSystem<T extends object>(
|
|
290
|
-
declaration: T,
|
|
291
|
-
bind: (scope: object) => T,
|
|
292
|
-
): T {
|
|
293
|
-
scopedSystems.set(declaration, bind);
|
|
294
|
-
return declaration;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Ordinary, already-bound adapters retain their exact identity. */
|
|
298
|
-
export function bindScopedSystem<T extends object>(declaration: T, scope: object): T {
|
|
299
|
-
const bind = scopedSystems.get(declaration);
|
|
300
|
-
return bind ? (bind(scope) as T) : declaration;
|
|
301
|
-
}
|
package/src/core/system-phase.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* THE SYSTEM-PHASE VOCABULARY — the order a frame runs in, and the shape a
|
|
3
|
-
* project's own system declares itself with.
|
|
4
|
-
*
|
|
5
|
-
* It lives in the CONTRACT because two runtimes speak it and neither owns it:
|
|
6
|
-
* `@volter/game-runtime`'s loop and system runner schedule by phase, and
|
|
7
|
-
* `@volter/threejs-runtime`'s animation clock orders its evaluators by the same
|
|
8
|
-
* `PHASE_ORDER`. A vocabulary two shipped twins both read is the contract's,
|
|
9
|
-
* or it is a value edge between twins — and that edge is the one thing the
|
|
10
|
-
* twin row may not have (docs/ARCHITECTURE-CORE.md §The target shape: a twin
|
|
11
|
-
* depends on the project contract and its medium's libraries).
|
|
12
|
-
*
|
|
13
|
-
* `scripts/generate-engine-manifest.ts` renders this file into
|
|
14
|
-
* `packages/project/schemas/engine-api.json`.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* System execution phases, in order.
|
|
19
|
-
* Each frame, systems run in this exact sequence.
|
|
20
|
-
*/
|
|
21
|
-
export const SystemPhase = {
|
|
22
|
-
INPUT: 'input',
|
|
23
|
-
PRE_PHYSICS: 'prePhysics',
|
|
24
|
-
PHYSICS: 'physics',
|
|
25
|
-
POST_PHYSICS: 'postPhysics',
|
|
26
|
-
GAME_LOGIC: 'gameLogic',
|
|
27
|
-
ANIMATION: 'animation',
|
|
28
|
-
PRE_RENDER: 'preRender',
|
|
29
|
-
RENDER: 'render',
|
|
30
|
-
} as const;
|
|
31
|
-
|
|
32
|
-
export type SystemPhaseName = (typeof SystemPhase)[keyof typeof SystemPhase];
|
|
33
|
-
|
|
34
|
-
/** Order of phase execution */
|
|
35
|
-
export const PHASE_ORDER: SystemPhaseName[] = [
|
|
36
|
-
SystemPhase.INPUT,
|
|
37
|
-
SystemPhase.PRE_PHYSICS,
|
|
38
|
-
SystemPhase.PHYSICS,
|
|
39
|
-
SystemPhase.POST_PHYSICS,
|
|
40
|
-
SystemPhase.GAME_LOGIC,
|
|
41
|
-
SystemPhase.ANIMATION,
|
|
42
|
-
SystemPhase.PRE_RENDER,
|
|
43
|
-
SystemPhase.RENDER,
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
/** A system is just a function that takes delta time */
|
|
47
|
-
export type SystemFn = (dt: number) => void;
|
|
48
|
-
|
|
49
|
-
export interface SystemOptions {
|
|
50
|
-
/** Stable diagnostic label; falls back to the function name. */
|
|
51
|
-
readonly name?: string;
|
|
52
|
-
}
|