@volter/editor-game 0.5.66 → 0.5.67
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/contributions/audio-unlock.service.ts +2 -2
- package/contributions/autoplay.service.ts +1 -1
- package/contributions/bridge.command.ts +3 -3
- package/contributions/canvas/component-board.service.ts +16 -0
- package/contributions/canvas/design-time-mount.service.ts +45 -0
- package/contributions/canvas-story-capture.service.ts +12 -0
- package/contributions/edit-mode-audio.service.ts +2 -2
- package/contributions/edit-mode-networking.service.ts +1 -1
- package/contributions/gameplay.command.ts +4 -4
- package/contributions/generation.service.ts +1 -1
- package/contributions/godot.style.ts +26 -6
- package/contributions/godot.view.ts +6 -0
- package/contributions/ingest.service.ts +2 -2
- package/contributions/instances.command.ts +1 -1
- package/contributions/navmesh.menu.ts +1 -1
- package/contributions/network-observer.service.ts +14 -0
- package/contributions/play.command.ts +10 -4
- package/contributions/react/component-board.service.ts +1 -1
- package/contributions/react/design-time-mount.service.ts +1 -1
- package/contributions/scene-document.service.ts +3 -3
- package/contributions/state-watch.menu.ts +1 -1
- package/contributions/state-watch.utility.tsx +1 -1
- package/contributions/team-playtest.service.ts +4 -4
- package/contributions/three/component-board.service.ts +1 -1
- package/contributions/three/component-verbs.command.ts +6 -6
- package/contributions/three/three-authoring.service.ts +8 -5
- package/contributions/three-story-capture.service.ts +12 -0
- package/contributions/unity.style.ts +17 -7
- package/contributions/unity.view.ts +6 -0
- package/contributions/unreal.style.ts +10 -2
- package/contributions/unreal.view.ts +5 -0
- package/package.json +20 -10
- package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
- package/src/asset-budget/asset-budget-model.ts +1 -1
- package/src/audio/AudioDebuggerPanel.tsx +1 -1
- package/src/bridge/dispatch.ts +14 -14
- package/src/bridge/live-frames.ts +1 -1
- package/src/bridge/screenshot.ts +3 -3
- package/src/build/BuildProfilesPanel.tsx +3 -3
- package/src/canvas/canvas-board/CanvasBoardDocument.tsx +676 -0
- package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
- package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
- package/src/canvas/canvas-design-mount.ts +524 -0
- package/src/canvas/design-time-canvas-mount.ts +79 -0
- package/src/coverage/live-authoring-surface.ts +5 -5
- package/src/coverage/live-project-verbs.ts +1 -1
- package/src/coverage/native-system-coverage.ts +5 -5
- package/src/coverage/root-coverage.ts +1 -1
- package/src/coverage/session-coverage.ts +3 -3
- package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
- package/src/edit-mode/edit-mode-audio.ts +4 -4
- package/src/edit-mode/edit-mode-networking.ts +2 -2
- package/src/game-document/GameCaptureFrameButton.tsx +1 -1
- package/src/game-document/GameDocument.tsx +2 -2
- package/src/game-document/GamePanel.tsx +4 -4
- package/src/game-document/InstanceInspectorPicker.tsx +2 -2
- package/src/game-document/crowd-debug.ts +1 -1
- package/src/game-document/device-preview.ts +10 -11
- package/src/game-document/physics-debug.ts +1 -1
- package/src/generation/GenerationActivity.tsx +3 -3
- package/src/generation/generation-documents.tsx +5 -5
- package/src/generation/generation-jobs.ts +1 -1
- package/src/host/adapter-runtime-bindings.ts +96 -5
- package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
- package/src/host/authoring/gesture-persist.ts +1 -1
- package/src/host/authoring/ingest-data-writer.ts +1 -1
- package/src/host/authoring/ingest-source-persistence.ts +4 -4
- package/src/host/authoring/mounted-authoring.ts +4 -4
- package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
- package/src/host/authoring/pixi-authoring-adapter.ts +40 -15
- package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
- package/src/host/authoring/pixi-live-write-target.ts +4 -4
- package/src/host/authoring/pixi-source-identity.ts +3 -3
- package/src/host/authoring/pixi-source-write-target.ts +1614 -0
- package/src/host/authoring/pixi-still-presentation.ts +1 -1
- package/src/host/authoring/pixi-structure-history.ts +2 -2
- package/src/host/authoring/pixi-transform-channels.ts +16 -14
- package/src/host/authoring/source-persistence-backend.ts +3 -3
- package/src/host/authoring/struct-write-pipe.ts +1 -1
- package/src/host/binding-resolver.ts +8 -9
- package/src/host/browser-transpile.ts +1 -1
- package/src/host/canvas-entry-runtime.ts +58 -47
- package/src/host/canvas-preview-frames.ts +482 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
- package/src/host/components/HeaderTelemetry.tsx +4 -4
- package/src/host/components/PixiIsolationSceneContent.tsx +10 -10
- package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
- package/src/host/components/frame-debugger-model.ts +2 -2
- package/src/host/components/header-telemetry-model.ts +2 -2
- package/src/host/components/scene-document.tsx +14 -14
- package/src/host/components/utility-view-state.ts +1 -1
- package/src/host/components/world-root-stage-binding.tsx +12 -12
- package/src/host/components/world-root-stage.ts +70 -49
- package/src/host/coverage/system-adapter-coverage.ts +3 -4
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
- package/src/host/document-preview-three.ts +1 -1
- package/src/host/entry-adjudication.ts +6 -6
- package/src/host/game-css-scope-transform.ts +4 -0
- package/src/host/game-realm-page.ts +1 -1
- package/src/host/gameplay-export.ts +25 -14
- package/src/host/gameplay-recording.ts +5 -5
- package/src/host/gated-globals.ts +2 -2
- package/src/host/history/json-history-resource.ts +1 -1
- package/src/host/projection/pixi.ts +24 -2
- package/src/host/r3f-entry-runtime.ts +65 -34
- package/src/host/react-mount-runtime.ts +7 -48
- package/src/host/realm-services.ts +1 -1
- package/src/host/roots/canvas-root.tsx +361 -0
- package/src/host/roots/r3f-root.tsx +473 -0
- package/src/host/roots/react-root.ts +9 -43
- package/src/host/served-bundle-runtime-modules.ts +1 -19
- package/src/host/server-log-bridge.ts +2 -2
- package/src/host/stories/mounted-story-viewport-source.ts +1 -1
- package/src/host/stories/pixi-story-model.ts +30 -0
- package/src/host/stories/story-media-captures.ts +46 -0
- package/src/host/stories/story-media-presence.ts +3 -3
- package/src/host/stories/story-pixi-preview.ts +408 -0
- package/src/host/stories/story-three-preview.ts +806 -0
- package/src/host/stories/three-story-captures.ts +35 -0
- package/src/host/story-three-preview-runtime.ts +56 -0
- package/src/host/use-active-performance-source.ts +2 -2
- package/src/host/viewport-pose-memory.ts +1 -1
- package/src/host/viewport-root-presentation.ts +6 -5
- package/src/ingest/active-ingest.ts +1 -1
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
- package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
- package/src/ingest/deferred-ingest-play.ts +6 -6
- package/src/ingest/discovery-public-ingest.ts +2 -2
- package/src/ingest/ingest-boot-viewport.ts +2 -2
- package/src/ingest/ingest-canvas-scene-document.tsx +8 -8
- package/src/ingest/ingest-canvas-scene.ts +3 -3
- package/src/ingest/ingest-evidence-hook.ts +1 -1
- package/src/ingest/ingest-frame-snapshot.ts +1 -1
- package/src/ingest/ingest-play-control.ts +1 -1
- package/src/ingest/ingest-render-debug.ts +10 -10
- package/src/ingest/ingest-siblings.ts +13 -25
- package/src/ingest/module-mode.ts +14 -14
- package/src/ingest/mount-canvas-ingest-root.ts +21 -21
- package/src/ingest/mount-coverage.ts +2 -2
- package/src/ingest/mount-dom-ingest-root.ts +11 -11
- package/src/ingest/mount-ingest-root.ts +8 -8
- package/src/ingest/mount-three-ingest-root.ts +8 -8
- package/src/ingest/resolve-canvas.ts +1 -1
- package/src/ingest/served-html-boot.ts +1 -1
- package/src/ingest/surface-canvas.ts +1 -1
- package/src/ingest/unmount-ingest-root.ts +5 -5
- package/src/navmesh/navmesh-handler.ts +24 -16
- package/src/network/NetworkInspectorPanel.tsx +449 -14
- package/src/network/network-inspector-model.ts +14 -2
- package/src/play/play-log-events.ts +1 -1
- package/src/play/play-mode.ts +76 -133
- package/src/play/play-recording.ts +1 -1
- package/src/play/react-play-live-authoring.ts +3 -3
- package/src/play/run-selection.ts +93 -0
- package/src/play-bar/PlayBar.tsx +20 -39
- package/src/profiler/FrameDebuggerPanel.tsx +1 -1
- package/src/profiler/PerformancePanel.tsx +3 -3
- package/src/react/design-time-react-mount.ts +23 -65
- package/src/react/dom-authoring-adapter.ts +9 -9
- package/src/react/react-inspector-section.tsx +5 -5
- package/src/react/react-world-authoring-adapter.ts +11 -11
- package/src/react/story-documents/story-documents.tsx +9 -9
- package/src/react/ui-board-document.tsx +7 -7
- package/src/react/ui-component-board.ts +2 -2
- package/src/runtime/adapter/audio-meter.ts +21 -0
- package/src/runtime/adapter/first-party-audio-system.ts +230 -0
- package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
- package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
- package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
- package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
- package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
- package/src/runtime/adapter/native-debug-module.ts +498 -0
- package/src/runtime/audio/bus-mixer.ts +161 -0
- package/src/runtime/audio/pose-guard.ts +80 -0
- package/src/runtime/core/frame-pacing.ts +126 -0
- package/src/runtime/core/game-loop.ts +225 -0
- package/src/runtime/core/game-scoped-slot.ts +28 -0
- package/src/runtime/core/seeded-random.ts +162 -0
- package/src/runtime/core/sim-clock.ts +391 -0
- package/src/runtime/core/system-runner.ts +269 -0
- package/src/runtime/core/types.ts +104 -0
- package/src/runtime/create-runtime.ts +1128 -0
- package/src/runtime/debug-bridge.ts +570 -0
- package/src/runtime/debug-registry.ts +899 -0
- package/src/runtime/dev/chrome-trace.ts +153 -0
- package/src/runtime/dev/instruments.ts +403 -0
- package/src/runtime/dev/logger.ts +119 -0
- package/src/runtime/dev/performance-profiler.ts +367 -0
- package/src/runtime/dev/register-render-vitals.ts +276 -0
- package/src/runtime/dev/render-census.ts +354 -0
- package/src/runtime/dev/render-debug-adapter.ts +218 -0
- package/src/runtime/dev/render-memory.ts +226 -0
- package/src/runtime/dev/render-vitals.ts +338 -0
- package/src/runtime/dev/static-batch-advisor.ts +188 -0
- package/src/runtime/dev/webgl-frame-capture.ts +366 -0
- package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
- package/src/runtime/dev-build.ts +47 -0
- package/src/runtime/game.ts +1636 -0
- package/src/runtime/gameplay-rng-trap.ts +135 -0
- package/src/runtime/host-context.ts +64 -0
- package/src/runtime/input-router.ts +169 -0
- package/src/runtime/mount-manifest.ts +480 -0
- package/src/runtime/pixi/authoring.ts +706 -0
- package/src/runtime/pixi/ingest.ts +116 -0
- package/src/runtime/pixi/physics-registry.ts +49 -0
- package/src/runtime/pixi/render-pass-bracket.ts +117 -0
- package/src/runtime/pixi/scene-capture.ts +179 -0
- package/src/runtime/pixi/system-adapters.ts +69 -0
- package/src/runtime/playtest.ts +22 -0
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-control.ts +642 -0
- package/src/runtime/render-seed.ts +77 -0
- package/src/runtime/run-ticks-settled.ts +73 -0
- package/src/runtime/setup/setup-audio.ts +72 -0
- package/src/services/audio-pose-guard.ts +2 -2
- package/src/services/game-audio.ts +152 -0
- package/src/services/game-network.ts +657 -0
- package/src/services/game-physics.ts +334 -0
- package/src/state-watch/StateWatchPanel.tsx +1 -1
- package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
- package/src/three/authoring/constraint-inspector-section.tsx +6 -5
- package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
- package/src/three/authoring/oid-source-persistence.ts +7 -7
- package/src/three/authoring/r3f-design-session.ts +58 -54
- package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
- package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
- package/src/three/authoring/three-authoring-adapter.ts +29 -29
- package/src/three/component-verbs/extract-menu.ts +7 -6
- package/src/three/component-verbs/fork-menu.ts +7 -6
- package/src/three/component-verbs/internals-menu.ts +2 -2
- package/src/three/story-documents/three-story-documents.tsx +13 -13
- package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
- package/src/three/three-board/board-scene.ts +6 -6
- package/src/three/three-board/three-component-board.ts +2 -2
- package/src/services/game-audio-unlock.ts +0 -48
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome Trace Event Format exporter (W4b, F11 profiler → chrome://tracing /
|
|
3
|
+
* Perfetto). A PURE, deterministic function over recorded
|
|
4
|
+
* {@link PerformanceFrame}s — no clock reads, no randomness, no I/O — so the
|
|
5
|
+
* same frames always serialize to byte-identical output (asserted by the unit
|
|
6
|
+
* test).
|
|
7
|
+
*
|
|
8
|
+
* Format (https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU):
|
|
9
|
+
* - `M` metadata events name the process and thread.
|
|
10
|
+
* - One `X` (complete) event per frame — `ts` at the frame's absolute start,
|
|
11
|
+
* `dur` the frame's CPU time — with nested `X` events per phase and per
|
|
12
|
+
* system placed at their MEASURED `startMs` offsets inside the frame.
|
|
13
|
+
* - One `C` (counter) event per frame carries the render draw/triangle
|
|
14
|
+
* counters, so the trace viewer draws them as a graph track.
|
|
15
|
+
*
|
|
16
|
+
* All times are microseconds (the format's unit); our profiler measures ms, so
|
|
17
|
+
* every ms value is × 1000. Every event carries the full key set
|
|
18
|
+
* (`ph/ts/dur/pid/tid/name/cat`) for uniformity — metadata/counter events
|
|
19
|
+
* would normally omit `dur`, but a constant `dur: 0` is harmless to the viewer
|
|
20
|
+
* and lets a consumer read every event without per-`ph` branching.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { PerformanceFrame } from './performance-profiler';
|
|
24
|
+
|
|
25
|
+
export interface ChromeTraceEvent {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly cat: string;
|
|
28
|
+
readonly ph: 'X' | 'M' | 'C';
|
|
29
|
+
/** Timestamp, microseconds. */
|
|
30
|
+
readonly ts: number;
|
|
31
|
+
/** Duration, microseconds (0 for metadata/counter events). */
|
|
32
|
+
readonly dur: number;
|
|
33
|
+
readonly pid: number;
|
|
34
|
+
readonly tid: number;
|
|
35
|
+
readonly args?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ChromeTraceFile {
|
|
39
|
+
readonly traceEvents: readonly ChromeTraceEvent[];
|
|
40
|
+
readonly displayTimeUnit: 'ms';
|
|
41
|
+
readonly otherData: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ChromeTraceOptions {
|
|
45
|
+
processName: string;
|
|
46
|
+
threadName: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const PID = 1;
|
|
50
|
+
const TID = 1;
|
|
51
|
+
const MS_TO_US = 1000;
|
|
52
|
+
|
|
53
|
+
function ms(value: number): number {
|
|
54
|
+
return Math.round(value * MS_TO_US);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildChromeTrace(
|
|
58
|
+
frames: readonly PerformanceFrame[],
|
|
59
|
+
options: ChromeTraceOptions,
|
|
60
|
+
): ChromeTraceFile {
|
|
61
|
+
const events: ChromeTraceEvent[] = [];
|
|
62
|
+
|
|
63
|
+
// Metadata: name the process + thread the frames live on.
|
|
64
|
+
events.push({
|
|
65
|
+
name: 'process_name',
|
|
66
|
+
cat: '__metadata',
|
|
67
|
+
ph: 'M',
|
|
68
|
+
ts: 0,
|
|
69
|
+
dur: 0,
|
|
70
|
+
pid: PID,
|
|
71
|
+
tid: TID,
|
|
72
|
+
args: { name: options.processName },
|
|
73
|
+
});
|
|
74
|
+
events.push({
|
|
75
|
+
name: 'thread_name',
|
|
76
|
+
cat: '__metadata',
|
|
77
|
+
ph: 'M',
|
|
78
|
+
ts: 0,
|
|
79
|
+
dur: 0,
|
|
80
|
+
pid: PID,
|
|
81
|
+
tid: TID,
|
|
82
|
+
args: { name: options.threadName },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
for (const frame of frames) {
|
|
86
|
+
// Absolute frame start (ms): timestamp is the frame's END, cpuMs its span.
|
|
87
|
+
const frameStartMs = frame.timestamp - frame.cpuMs;
|
|
88
|
+
const frameStartUs = ms(frameStartMs);
|
|
89
|
+
|
|
90
|
+
events.push({
|
|
91
|
+
name: `frame ${frame.id}`,
|
|
92
|
+
cat: 'frame',
|
|
93
|
+
ph: 'X',
|
|
94
|
+
ts: frameStartUs,
|
|
95
|
+
dur: ms(frame.cpuMs),
|
|
96
|
+
pid: PID,
|
|
97
|
+
tid: TID,
|
|
98
|
+
args: {
|
|
99
|
+
id: frame.id,
|
|
100
|
+
drawCalls: frame.render.drawCalls,
|
|
101
|
+
triangles: frame.render.triangles,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
for (const phase of frame.phases) {
|
|
106
|
+
events.push({
|
|
107
|
+
name: phase.name,
|
|
108
|
+
cat: 'phase',
|
|
109
|
+
ph: 'X',
|
|
110
|
+
ts: ms(frameStartMs + (phase.startMs ?? 0)),
|
|
111
|
+
dur: ms(phase.ms),
|
|
112
|
+
pid: PID,
|
|
113
|
+
tid: TID,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const system of frame.systems) {
|
|
118
|
+
events.push({
|
|
119
|
+
name: system.name,
|
|
120
|
+
cat: 'system',
|
|
121
|
+
ph: 'X',
|
|
122
|
+
ts: ms(frameStartMs + system.startMs),
|
|
123
|
+
dur: ms(system.ms),
|
|
124
|
+
pid: PID,
|
|
125
|
+
tid: TID,
|
|
126
|
+
args: { phase: system.phase },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Counter track: render draw/triangle counts at this frame's start.
|
|
131
|
+
events.push({
|
|
132
|
+
name: 'render',
|
|
133
|
+
cat: 'counter',
|
|
134
|
+
ph: 'C',
|
|
135
|
+
ts: frameStartUs,
|
|
136
|
+
dur: 0,
|
|
137
|
+
pid: PID,
|
|
138
|
+
tid: TID,
|
|
139
|
+
args: { drawCalls: frame.render.drawCalls, triangles: frame.render.triangles },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
traceEvents: events,
|
|
145
|
+
displayTimeUnit: 'ms',
|
|
146
|
+
otherData: {
|
|
147
|
+
version: 2,
|
|
148
|
+
processName: options.processName,
|
|
149
|
+
threadName: options.threadName,
|
|
150
|
+
frameCount: frames.length,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE UNIVERSAL INSTRUMENTS — time scale, pause/frame-step, collider draw and
|
|
3
|
+
* frame time, furnished by the ENGINE for every game before it has written a
|
|
4
|
+
* line.
|
|
5
|
+
*
|
|
6
|
+
* These are physics of the medium, not genre ontology. Time runs; it can run
|
|
7
|
+
* slower or stop; a frame can be stepped; colliders can be drawn; frames take
|
|
8
|
+
* time. None of that is a decision any particular game makes, so none of it
|
|
9
|
+
* belongs in a game's own declarations — and none of it belongs on the game's
|
|
10
|
+
* own dev panel either, which shows ONLY what the game itself declared
|
|
11
|
+
* (`stat()`, `cheat()`). They publish as DOORS: an agent drives them through
|
|
12
|
+
* `game.state()`/`game.command()`, and a human drives the same controls from
|
|
13
|
+
* the editor chrome that already owns them — pause and frame-step on the
|
|
14
|
+
* transport, frame time on the stats overlay, colliders in the play-adopted
|
|
15
|
+
* Scene viewport.
|
|
16
|
+
*
|
|
17
|
+
* WHAT IS NOT HERE, and why: a FREE CAMERA. The editor already ships one
|
|
18
|
+
* (owner, 2026-08-10): play adoption swaps the live game scene into the editor
|
|
19
|
+
* viewport (`enterPlayScene`), so the Scene view during play IS a free orbit
|
|
20
|
+
* camera over the running game — no game-side camera override needed, and the
|
|
21
|
+
* game's own camera writes never fight it because the game keeps rendering
|
|
22
|
+
* through its own canvas untouched. Per the non-redundancy rule that governs
|
|
23
|
+
* this list, a control the editor's own chrome furnishes gets no instrument;
|
|
24
|
+
* agents reach that camera through the editor half of `vgai eval`, not the
|
|
25
|
+
* game registry.
|
|
26
|
+
*
|
|
27
|
+
* ── THE DOOR ────────────────────────────────────────────────────────────────
|
|
28
|
+
* The HOST publishes these itself ({@link publishDevInstruments}, called by
|
|
29
|
+
* the mount install paths): one provider per reading
|
|
30
|
+
* (`instrument.timescale`), one command per action
|
|
31
|
+
* (`instrument.timescale.set`), plus the `instruments` index provider. The
|
|
32
|
+
* registry IS the product, so every reader of an instrument reaches the SAME
|
|
33
|
+
* registration — and a game writes zero lines and mounts nothing to have
|
|
34
|
+
* them.
|
|
35
|
+
*
|
|
36
|
+
* ── DEGRADING ───────────────────────────────────────────────────────────────
|
|
37
|
+
* An instrument whose subject this game does not have (no physics adapter ⇒ no
|
|
38
|
+
* colliders) READS `null` — honest emptiness rather than a fabricated value.
|
|
39
|
+
* Its ACTION, invoked anyway, THROWS and names what is missing. Silence is the
|
|
40
|
+
* one thing forbidden: an agent that calls `instrument.colliders.toggle` and
|
|
41
|
+
* gets `undefined` back learns nothing.
|
|
42
|
+
*
|
|
43
|
+
* ── RESOURCE OWNERSHIP, STATED ONCE ─────────────────────────────────────────
|
|
44
|
+
* {@link createDevInstruments} owns everything it allocates: the frame-time
|
|
45
|
+
* ring buffer and its ONE `game.onRenderStep` subscription. SHARER: the host
|
|
46
|
+
* install path ({@link publishDevInstruments}), which creates one set per
|
|
47
|
+
* mounted game. TEARDOWN: the returned {@link DevInstrumentSet.dispose} — the
|
|
48
|
+
* ONE path that ends it. Nothing else here holds a resource; the time-scale, pause and
|
|
49
|
+
* collider instruments only drive state that `Game` already owns.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { nodeKeyedPhysics } from '@volter/editor-project/adapter/system-adapter';
|
|
53
|
+
import { z } from 'zod';
|
|
54
|
+
import { clampTimeScale, TIME_SCALE_RANGE } from '../core/frame-pacing';
|
|
55
|
+
import { getDebugRegistry } from '../debug-registry';
|
|
56
|
+
import type { Game } from '../game';
|
|
57
|
+
|
|
58
|
+
/** What shape an instrument's reading is, and what drives it. Data, not
|
|
59
|
+
* layout: a reader learns how to interpret and actuate an instrument without
|
|
60
|
+
* having to recognise it by name. */
|
|
61
|
+
export type DevInstrumentControl =
|
|
62
|
+
/** A value only. */
|
|
63
|
+
| { readonly kind: 'reading' }
|
|
64
|
+
/** On/off, driven by the named action. */
|
|
65
|
+
| { readonly kind: 'toggle'; readonly action: string }
|
|
66
|
+
/** A continuous value, driven by the named action (one numeric argument). */
|
|
67
|
+
| {
|
|
68
|
+
readonly kind: 'scale';
|
|
69
|
+
readonly action: string;
|
|
70
|
+
readonly min: number;
|
|
71
|
+
readonly max: number;
|
|
72
|
+
readonly step: number;
|
|
73
|
+
}
|
|
74
|
+
/** A {@link FrameTimeReading} — a window of samples, not a scalar. */
|
|
75
|
+
| { readonly kind: 'plot' };
|
|
76
|
+
|
|
77
|
+
/** One instrument verb. Every action declares an args tuple — `z.tuple([])`
|
|
78
|
+
* for a verb that takes none — so the debug registry REJECTS an unexpected
|
|
79
|
+
* argument at the door instead of passing it through to a handler that
|
|
80
|
+
* ignores it. */
|
|
81
|
+
export interface DevInstrumentAction {
|
|
82
|
+
/** Second half of the command name: `instrument.<id>.<this>`. */
|
|
83
|
+
readonly id: string;
|
|
84
|
+
readonly label: string;
|
|
85
|
+
readonly hint: string;
|
|
86
|
+
readonly args: z.ZodTuple;
|
|
87
|
+
run(values: readonly unknown[]): unknown;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface DevInstrument {
|
|
91
|
+
/** Second half of the provider name: `instrument.<this>`. */
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly label: string;
|
|
94
|
+
readonly hint: string;
|
|
95
|
+
/** Trailing unit for a scalar reading (`x`, `ms`). */
|
|
96
|
+
readonly unit?: string;
|
|
97
|
+
readonly control: DevInstrumentControl;
|
|
98
|
+
readonly actions: readonly DevInstrumentAction[];
|
|
99
|
+
/** This instant's value, or `null` when this game has no such subject. */
|
|
100
|
+
read(): unknown;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface DevInstrumentSet {
|
|
104
|
+
readonly instruments: readonly DevInstrument[];
|
|
105
|
+
/** See the ownership note above — the ONE teardown path. */
|
|
106
|
+
dispose(): void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* How many display frames the frame-time reading keeps. ~1 second at 60 Hz —
|
|
111
|
+
* long enough that a hitch is still in the window when you look, short enough
|
|
112
|
+
* that the mean tracks what the game is doing NOW.
|
|
113
|
+
*/
|
|
114
|
+
export const FRAME_TIME_WINDOW = 60;
|
|
115
|
+
|
|
116
|
+
/** What `instrument.frame-time` reads. `null`s (rather than zeros) before the
|
|
117
|
+
* first sampled frame: a stopped game has no frame time, and `0 ms / 0 fps`
|
|
118
|
+
* would read as a measurement. */
|
|
119
|
+
export interface FrameTimeReading {
|
|
120
|
+
/** Mean wall-clock milliseconds per display frame over the window. */
|
|
121
|
+
readonly ms: number | null;
|
|
122
|
+
/** Frames per second implied by {@link ms}. */
|
|
123
|
+
readonly fps: number | null;
|
|
124
|
+
/** The worst single frame in the window — the hitch the mean hides. */
|
|
125
|
+
readonly worstMs: number | null;
|
|
126
|
+
/** Oldest first, for the plot. */
|
|
127
|
+
readonly samples: readonly number[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Pure: the whole frame-time reading from a window of samples. */
|
|
131
|
+
export function summarizeFrameTimes(samples: readonly number[]): FrameTimeReading {
|
|
132
|
+
if (samples.length === 0) return { ms: null, fps: null, worstMs: null, samples: [] };
|
|
133
|
+
let total = 0;
|
|
134
|
+
let worst = 0;
|
|
135
|
+
for (const sample of samples) {
|
|
136
|
+
total += sample;
|
|
137
|
+
if (sample > worst) worst = sample;
|
|
138
|
+
}
|
|
139
|
+
const ms = total / samples.length;
|
|
140
|
+
return {
|
|
141
|
+
ms: round(ms, 2),
|
|
142
|
+
fps: ms > 0 ? round(1000 / ms, 1) : null,
|
|
143
|
+
worstMs: round(worst, 2),
|
|
144
|
+
samples: samples.map((sample) => round(sample, 2)),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function round(value: number, places: number): number {
|
|
149
|
+
const factor = 10 ** places;
|
|
150
|
+
return Math.round(value * factor) / factor;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const NO_COLLIDERS =
|
|
154
|
+
'No physics adapter on this game implements setDebugDrawEnabled, so nothing can draw its ' +
|
|
155
|
+
'colliders. A first-party three root with Rapier physics registers one; a game with no ' +
|
|
156
|
+
'physics has no colliders to draw.';
|
|
157
|
+
|
|
158
|
+
const NO_ARGS = z.tuple([]);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build this game's universal instrument set. One call per mounted game; the
|
|
162
|
+
* caller owns the returned {@link DevInstrumentSet.dispose}.
|
|
163
|
+
*/
|
|
164
|
+
export function createDevInstruments(game: Game): DevInstrumentSet {
|
|
165
|
+
// --- Frame time: one subscription, one ring buffer ------------------------
|
|
166
|
+
const samples: number[] = [];
|
|
167
|
+
let previousFrameAt = now();
|
|
168
|
+
const stopSampling = game.onRenderStep(() => {
|
|
169
|
+
const at = now();
|
|
170
|
+
samples.push(at - previousFrameAt);
|
|
171
|
+
previousFrameAt = at;
|
|
172
|
+
if (samples.length > FRAME_TIME_WINDOW) samples.shift();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// --- Collider draw: the adapter has a setter and no getter, so the ON/OFF
|
|
176
|
+
// state is ours to remember. It is only ever written through the one action
|
|
177
|
+
// below, which is also the only thing that calls the adapter.
|
|
178
|
+
let collidersDrawn = false;
|
|
179
|
+
const colliderDraw = () => {
|
|
180
|
+
// Node-id keyed only — `setDebugDrawEnabled` draws THREE wireframes into
|
|
181
|
+
// the world, which a canvas mount's display-keyed carrier has no analogue
|
|
182
|
+
// for and does not declare.
|
|
183
|
+
const adapter = nodeKeyedPhysics(game.systemAdapters.physics);
|
|
184
|
+
return adapter?.setDebugDrawEnabled ? adapter : null;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const instruments: readonly DevInstrument[] = [
|
|
188
|
+
{
|
|
189
|
+
id: 'timescale',
|
|
190
|
+
label: 'Time scale',
|
|
191
|
+
hint: 'Sim speed multiplier. 1 is real time, 0 freezes the simulation while the game keeps drawing.',
|
|
192
|
+
unit: 'x',
|
|
193
|
+
control: {
|
|
194
|
+
kind: 'scale',
|
|
195
|
+
action: 'set',
|
|
196
|
+
min: TIME_SCALE_RANGE.min,
|
|
197
|
+
max: TIME_SCALE_RANGE.max,
|
|
198
|
+
step: 0.05,
|
|
199
|
+
},
|
|
200
|
+
actions: [
|
|
201
|
+
{
|
|
202
|
+
id: 'set',
|
|
203
|
+
label: 'Set',
|
|
204
|
+
hint: `Set the sim speed multiplier (${TIME_SCALE_RANGE.min}–${TIME_SCALE_RANGE.max}).`,
|
|
205
|
+
args: z.tuple([z.number()]),
|
|
206
|
+
run: (values) => {
|
|
207
|
+
// The LOOP clamps and warns; asking for the clamp here too would
|
|
208
|
+
// be a second opinion about the same range. It reads back the
|
|
209
|
+
// value that actually took effect, never the one requested.
|
|
210
|
+
game.loop.timeScale = clampTimeScale(values[0] as number);
|
|
211
|
+
return game.loop.timeScale;
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
read: () => game.loop.timeScale,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: 'paused',
|
|
219
|
+
label: 'Paused',
|
|
220
|
+
hint: 'Freeze the simulation. The game keeps drawing, so you can look at a frozen frame.',
|
|
221
|
+
control: { kind: 'toggle', action: 'toggle' },
|
|
222
|
+
actions: [
|
|
223
|
+
{
|
|
224
|
+
id: 'toggle',
|
|
225
|
+
label: 'Toggle',
|
|
226
|
+
hint: 'Pause or resume the simulation.',
|
|
227
|
+
args: NO_ARGS,
|
|
228
|
+
run: () => {
|
|
229
|
+
if (game.play.paused) game.play.resume();
|
|
230
|
+
else game.play.pause();
|
|
231
|
+
return game.play.paused;
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'step',
|
|
236
|
+
label: 'Step',
|
|
237
|
+
hint: 'Advance exactly one fixed substep. Pauses first if the game is running.',
|
|
238
|
+
args: NO_ARGS,
|
|
239
|
+
run: () => {
|
|
240
|
+
// Frame advance implies pause — `Game.play.step()` is a
|
|
241
|
+
// deliberate whole-call no-op while running, and a button that
|
|
242
|
+
// silently does nothing is the failure this instrument exists to
|
|
243
|
+
// avoid.
|
|
244
|
+
if (!game.play.paused) game.play.pause();
|
|
245
|
+
game.play.step();
|
|
246
|
+
return game.play.paused;
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
read: () => game.play.paused,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: 'colliders',
|
|
254
|
+
label: 'Colliders drawn',
|
|
255
|
+
hint: "Draw the physics colliders over the world, through the game's own physics adapter.",
|
|
256
|
+
control: { kind: 'toggle', action: 'toggle' },
|
|
257
|
+
actions: [
|
|
258
|
+
{
|
|
259
|
+
id: 'toggle',
|
|
260
|
+
label: 'Toggle',
|
|
261
|
+
hint: 'Turn collider wireframes on or off.',
|
|
262
|
+
args: NO_ARGS,
|
|
263
|
+
run: () => {
|
|
264
|
+
const adapter = colliderDraw();
|
|
265
|
+
if (!adapter) throw new Error(NO_COLLIDERS);
|
|
266
|
+
collidersDrawn = !collidersDrawn;
|
|
267
|
+
adapter.setDebugDrawEnabled?.(collidersDrawn);
|
|
268
|
+
return collidersDrawn;
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
read: () => (colliderDraw() === null ? null : collidersDrawn),
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: 'frame-time',
|
|
276
|
+
label: 'Frame time',
|
|
277
|
+
hint: 'Wall-clock milliseconds per display frame, newest last. The window is where a hitch shows; the mean hides it.',
|
|
278
|
+
unit: 'ms',
|
|
279
|
+
control: { kind: 'plot' },
|
|
280
|
+
actions: [
|
|
281
|
+
{
|
|
282
|
+
id: 'clear',
|
|
283
|
+
label: 'Clear',
|
|
284
|
+
hint: 'Forget the sampled window and start measuring again.',
|
|
285
|
+
args: NO_ARGS,
|
|
286
|
+
run: () => {
|
|
287
|
+
samples.length = 0;
|
|
288
|
+
return null;
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
read: () => summarizeFrameTimes(samples),
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
instruments,
|
|
298
|
+
dispose() {
|
|
299
|
+
stopSampling();
|
|
300
|
+
samples.length = 0;
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function now(): number {
|
|
306
|
+
return typeof performance === 'undefined' ? Date.now() : performance.now();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Host publication — the instruments' OWN door
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
//
|
|
313
|
+
// The HOST publishes these (the editor's mount install path), so every game
|
|
314
|
+
// gets them for literally zero lines — no capability, no bridge component, no
|
|
315
|
+
// mount. One provider per reading (`instrument.timescale`), one command per
|
|
316
|
+
// action (`instrument.timescale.set`), plus ONE index provider
|
|
317
|
+
// (`instruments`) carrying every descriptor with this instant's values, so an
|
|
318
|
+
// outside reader gets the whole panel in one round trip. Naming is unchanged
|
|
319
|
+
// from when the dev-tools capability published them, so every existing reader
|
|
320
|
+
// keeps working; only the publisher moved realms.
|
|
321
|
+
|
|
322
|
+
/** The index provider's name. */
|
|
323
|
+
export const INSTRUMENTS_PROVIDER = 'instruments';
|
|
324
|
+
|
|
325
|
+
/** The registry scope the host publishes under. Not a mounted world —
|
|
326
|
+
* instruments are game-scoped host furniture. */
|
|
327
|
+
const INSTRUMENTS_WORLD_ID = '__instruments__';
|
|
328
|
+
|
|
329
|
+
/** One instrument in the index: descriptor plus this instant's value. A
|
|
330
|
+
* throwing read is reported as itself, never swallowed. */
|
|
331
|
+
export interface PublishedInstrumentReading {
|
|
332
|
+
readonly name: string;
|
|
333
|
+
readonly id: string;
|
|
334
|
+
readonly label: string;
|
|
335
|
+
readonly hint: string;
|
|
336
|
+
readonly unit?: string;
|
|
337
|
+
readonly control: DevInstrumentControl;
|
|
338
|
+
readonly actions: readonly {
|
|
339
|
+
readonly name: string;
|
|
340
|
+
readonly id: string;
|
|
341
|
+
readonly label: string;
|
|
342
|
+
readonly hint: string;
|
|
343
|
+
}[];
|
|
344
|
+
readonly value: unknown;
|
|
345
|
+
readonly error?: string;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function readingOf(instrument: DevInstrument): PublishedInstrumentReading {
|
|
349
|
+
const base = {
|
|
350
|
+
name: `instrument.${instrument.id}`,
|
|
351
|
+
id: instrument.id,
|
|
352
|
+
label: instrument.label,
|
|
353
|
+
hint: instrument.hint,
|
|
354
|
+
...(instrument.unit === undefined ? {} : { unit: instrument.unit }),
|
|
355
|
+
control: instrument.control,
|
|
356
|
+
actions: instrument.actions.map((action) => ({
|
|
357
|
+
name: `instrument.${instrument.id}.${action.id}`,
|
|
358
|
+
id: action.id,
|
|
359
|
+
label: action.label,
|
|
360
|
+
hint: action.hint,
|
|
361
|
+
})),
|
|
362
|
+
};
|
|
363
|
+
try {
|
|
364
|
+
return { ...base, value: instrument.read() };
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return { ...base, value: null, error: error instanceof Error ? error.message : String(error) };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Create this game's instrument set and register every door on its debug
|
|
372
|
+
* registry. Called once per mounted Game by the host install path; the
|
|
373
|
+
* returned disposer ends the set's one resource (the frame-time
|
|
374
|
+
* subscription) — the registrations die with the Game itself.
|
|
375
|
+
*/
|
|
376
|
+
export function publishDevInstruments(game: Game): () => void {
|
|
377
|
+
const registry = getDebugRegistry(game);
|
|
378
|
+
if (!registry) {
|
|
379
|
+
throw new Error('Cannot publish dev instruments: mounted Game has no debug registry.');
|
|
380
|
+
}
|
|
381
|
+
const set = createDevInstruments(game);
|
|
382
|
+
const scope = registry.forRoot(INSTRUMENTS_WORLD_ID);
|
|
383
|
+
for (const instrument of set.instruments) {
|
|
384
|
+
scope.registerStateProvider(`instrument.${instrument.id}`, () => instrument.read(), {
|
|
385
|
+
tier: 'assisted',
|
|
386
|
+
});
|
|
387
|
+
for (const action of instrument.actions) {
|
|
388
|
+
scope.registerCommand(
|
|
389
|
+
`instrument.${instrument.id}.${action.id}`,
|
|
390
|
+
{
|
|
391
|
+
description: `${instrument.label}: ${action.label} — ${action.hint}`,
|
|
392
|
+
locus: 'client',
|
|
393
|
+
args: action.args,
|
|
394
|
+
},
|
|
395
|
+
(...values: unknown[]) => action.run(values),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
scope.registerStateProvider(INSTRUMENTS_PROVIDER, () => set.instruments.map(readingOf), {
|
|
400
|
+
tier: 'assisted',
|
|
401
|
+
});
|
|
402
|
+
return () => set.dispose();
|
|
403
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { BlankTransport, LogLayer, LogLevel } from 'loglayer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flag set while the engine logger is calling console.* so that the
|
|
5
|
+
* console-bridge and play-mode patches can skip re-forwarding.
|
|
6
|
+
* JS is single-threaded so this is safe.
|
|
7
|
+
*/
|
|
8
|
+
export let _engineLogActive = false;
|
|
9
|
+
|
|
10
|
+
const CONSOLE_MAP: Record<string, 'log' | 'warn' | 'error'> = {
|
|
11
|
+
trace: 'log',
|
|
12
|
+
debug: 'log',
|
|
13
|
+
info: 'log',
|
|
14
|
+
warn: 'warn',
|
|
15
|
+
error: 'error',
|
|
16
|
+
fatal: 'error',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const engineLog = new LogLayer({
|
|
20
|
+
transport: new BlankTransport({
|
|
21
|
+
shipToLogger: ({ logLevel, messages, data, hasData }) => {
|
|
22
|
+
const method = CONSOLE_MAP[logLevel] ?? 'log';
|
|
23
|
+
|
|
24
|
+
// Extract _sub (subsystem tag) from data, pass the rest as metadata
|
|
25
|
+
let subsystem: string | undefined;
|
|
26
|
+
let metadata: Record<string, unknown> | undefined;
|
|
27
|
+
if (hasData && data) {
|
|
28
|
+
const { _sub, ...rest } = data as Record<string, unknown>;
|
|
29
|
+
subsystem = _sub ? String(_sub) : undefined;
|
|
30
|
+
if (Object.keys(rest).length > 0) metadata = rest;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Call console for browser devtools
|
|
34
|
+
_engineLogActive = true;
|
|
35
|
+
if (metadata) {
|
|
36
|
+
// biome-ignore lint/suspicious/noConsole: intentional — this IS the logger
|
|
37
|
+
console[method](...messages, metadata);
|
|
38
|
+
} else {
|
|
39
|
+
// biome-ignore lint/suspicious/noConsole: intentional — this IS the logger
|
|
40
|
+
console[method](...messages);
|
|
41
|
+
}
|
|
42
|
+
_engineLogActive = false;
|
|
43
|
+
|
|
44
|
+
// Post structured message for editor console
|
|
45
|
+
if (typeof window !== 'undefined') {
|
|
46
|
+
window.postMessage(
|
|
47
|
+
{
|
|
48
|
+
type: 'engine-log',
|
|
49
|
+
level: logLevel,
|
|
50
|
+
message: messages.join(' '),
|
|
51
|
+
subsystem,
|
|
52
|
+
metadata,
|
|
53
|
+
timestamp: Date.now(),
|
|
54
|
+
},
|
|
55
|
+
'*',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return messages;
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
prefix: '[engine]',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Default to 'info' — debug/trace are off unless explicitly enabled
|
|
66
|
+
engineLog.setLevel(LogLevel.info);
|
|
67
|
+
|
|
68
|
+
export interface LogMeta {
|
|
69
|
+
entityId?: string | number | undefined;
|
|
70
|
+
[key: string]: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface SubsystemLogger {
|
|
74
|
+
debug(msg: string, meta?: LogMeta): void;
|
|
75
|
+
info(msg: string, meta?: LogMeta): void;
|
|
76
|
+
warn(msg: string, meta?: LogMeta): void;
|
|
77
|
+
error(msg: string, meta?: LogMeta): void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createSubsystemLogger(subsystem: string): SubsystemLogger {
|
|
81
|
+
return {
|
|
82
|
+
debug: (msg, meta) =>
|
|
83
|
+
engineLog.withMetadata({ ...meta, _sub: subsystem }).debug(`[${subsystem}] ${msg}`),
|
|
84
|
+
info: (msg, meta) =>
|
|
85
|
+
engineLog.withMetadata({ ...meta, _sub: subsystem }).info(`[${subsystem}] ${msg}`),
|
|
86
|
+
warn: (msg, meta) =>
|
|
87
|
+
engineLog.withMetadata({ ...meta, _sub: subsystem }).warn(`[${subsystem}] ${msg}`),
|
|
88
|
+
error: (msg, meta) =>
|
|
89
|
+
engineLog.withMetadata({ ...meta, _sub: subsystem }).error(`[${subsystem}] ${msg}`),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Pre-built subsystem loggers — all share the same LogLayer instance
|
|
94
|
+
// so setLevel() on engineLog affects all of them.
|
|
95
|
+
export const log = {
|
|
96
|
+
scene: createSubsystemLogger('scene'),
|
|
97
|
+
physics: createSubsystemLogger('physics'),
|
|
98
|
+
animation: createSubsystemLogger('animation'),
|
|
99
|
+
network: createSubsystemLogger('network'),
|
|
100
|
+
audio: createSubsystemLogger('audio'),
|
|
101
|
+
ai: createSubsystemLogger('ai'),
|
|
102
|
+
input: createSubsystemLogger('input'),
|
|
103
|
+
core: createSubsystemLogger('core'),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export { engineLog };
|
|
107
|
+
|
|
108
|
+
// Expose to browser devtools: type setEngineLogLevel('debug') in console
|
|
109
|
+
if (typeof window !== 'undefined') {
|
|
110
|
+
(window as unknown as Record<string, unknown>)['setEngineLogLevel'] = (level: string) => {
|
|
111
|
+
engineLog.setLevel(level as LogLevel);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// URL param override: ?engineLogLevel=debug
|
|
116
|
+
if (typeof window !== 'undefined') {
|
|
117
|
+
const level = new URLSearchParams(window.location.search).get('engineLogLevel');
|
|
118
|
+
if (level) engineLog.setLevel(level as LogLevel);
|
|
119
|
+
}
|