@sadhaka/loom-engine 0.13.0 → 0.15.0
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/dist/audio/audio-asset-cache.d.ts +12 -0
- package/dist/audio/audio-asset-cache.d.ts.map +1 -0
- package/dist/audio/audio-asset-cache.js +53 -0
- package/dist/audio/audio-asset-cache.js.map +1 -0
- package/dist/audio/audio-asset-loader.d.ts +16 -0
- package/dist/audio/audio-asset-loader.d.ts.map +1 -0
- package/dist/audio/audio-asset-loader.js +100 -0
- package/dist/audio/audio-asset-loader.js.map +1 -0
- package/dist/audio/audio-listener-resource.d.ts +19 -0
- package/dist/audio/audio-listener-resource.d.ts.map +1 -0
- package/dist/audio/audio-listener-resource.js +57 -0
- package/dist/audio/audio-listener-resource.js.map +1 -0
- package/dist/audio/cue-catalog.d.ts +39 -0
- package/dist/audio/cue-catalog.d.ts.map +1 -0
- package/dist/audio/cue-catalog.js +186 -0
- package/dist/audio/cue-catalog.js.map +1 -0
- package/dist/audio/music-director.d.ts +18 -0
- package/dist/audio/music-director.d.ts.map +1 -0
- package/dist/audio/music-director.js +177 -0
- package/dist/audio/music-director.js.map +1 -0
- package/dist/audio/spatial-audio-bus.d.ts +66 -0
- package/dist/audio/spatial-audio-bus.d.ts.map +1 -0
- package/dist/audio/spatial-audio-bus.js +341 -0
- package/dist/audio/spatial-audio-bus.js.map +1 -0
- package/dist/audio/spatial-audio-system.d.ts +17 -0
- package/dist/audio/spatial-audio-system.d.ts.map +1 -0
- package/dist/audio/spatial-audio-system.js +98 -0
- package/dist/audio/spatial-audio-system.js.map +1 -0
- package/dist/audio/zone-audio-system.d.ts +78 -0
- package/dist/audio/zone-audio-system.d.ts.map +1 -0
- package/dist/audio/zone-audio-system.js +206 -0
- package/dist/audio/zone-audio-system.js.map +1 -0
- package/dist/director/ai/ai-plugin-registry.d.ts +20 -0
- package/dist/director/ai/ai-plugin-registry.d.ts.map +1 -0
- package/dist/director/ai/ai-plugin-registry.js +232 -0
- package/dist/director/ai/ai-plugin-registry.js.map +1 -0
- package/dist/director/ai/mock-ai-plugin.d.ts +24 -0
- package/dist/director/ai/mock-ai-plugin.d.ts.map +1 -0
- package/dist/director/ai/mock-ai-plugin.js +83 -0
- package/dist/director/ai/mock-ai-plugin.js.map +1 -0
- package/dist/director/ai/plugin-context.d.ts +27 -0
- package/dist/director/ai/plugin-context.d.ts.map +1 -0
- package/dist/director/ai/plugin-context.js +152 -0
- package/dist/director/ai/plugin-context.js.map +1 -0
- package/dist/director/ai/plugin.d.ts +57 -0
- package/dist/director/ai/plugin.d.ts.map +1 -0
- package/dist/director/ai/plugin.js +32 -0
- package/dist/director/ai/plugin.js.map +1 -0
- package/dist/director/index.d.ts +27 -0
- package/dist/director/index.d.ts.map +1 -0
- package/dist/director/index.js +26 -0
- package/dist/director/index.js.map +1 -0
- package/dist/director/zone/mock-zone-bridge.d.ts +22 -0
- package/dist/director/zone/mock-zone-bridge.d.ts.map +1 -0
- package/dist/director/zone/mock-zone-bridge.js +107 -0
- package/dist/director/zone/mock-zone-bridge.js.map +1 -0
- package/dist/director/zone/sse-zone-bridge.d.ts +40 -0
- package/dist/director/zone/sse-zone-bridge.d.ts.map +1 -0
- package/dist/director/zone/sse-zone-bridge.js +164 -0
- package/dist/director/zone/sse-zone-bridge.js.map +1 -0
- package/dist/director/zone/zone-event-bridge.d.ts +21 -0
- package/dist/director/zone/zone-event-bridge.d.ts.map +1 -0
- package/dist/director/zone/zone-event-bridge.js +24 -0
- package/dist/director/zone/zone-event-bridge.js.map +1 -0
- package/dist/director/zone/zone-event-envelope.d.ts +90 -0
- package/dist/director/zone/zone-event-envelope.d.ts.map +1 -0
- package/dist/director/zone/zone-event-envelope.js +104 -0
- package/dist/director/zone/zone-event-envelope.js.map +1 -0
- package/dist/director/zone/zone-event-log.d.ts +17 -0
- package/dist/director/zone/zone-event-log.d.ts.map +1 -0
- package/dist/director/zone/zone-event-log.js +46 -0
- package/dist/director/zone/zone-event-log.js.map +1 -0
- package/dist/director/zone/zone-event-system.d.ts +14 -0
- package/dist/director/zone/zone-event-system.d.ts.map +1 -0
- package/dist/director/zone/zone-event-system.js +179 -0
- package/dist/director/zone/zone-event-system.js.map +1 -0
- package/dist/director/zone/zone-state-resource.d.ts +15 -0
- package/dist/director/zone/zone-state-resource.d.ts.map +1 -0
- package/dist/director/zone/zone-state-resource.js +60 -0
- package/dist/director/zone/zone-state-resource.js.map +1 -0
- package/dist/index.d.ts +27 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -1
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +33 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +6 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SpatialAudioSystem - pushes the local character's transform into
|
|
2
|
+
// the AudioListener resource and the SpatialAudioBus each frame.
|
|
3
|
+
//
|
|
4
|
+
// Phasing rationale (matches PeerRenderSystem / animation-system):
|
|
5
|
+
// PHASE_RENDER, AFTER any camera/transform sync systems, BEFORE
|
|
6
|
+
// the renderer submits draw calls. The listener pose for THIS frame
|
|
7
|
+
// reflects the THIS-FRAME world transform of the player - so a
|
|
8
|
+
// positional sound triggered by a render-phase system (e.g. cue
|
|
9
|
+
// reactions to zone events drained earlier in PHASE_LOGIC) hears
|
|
10
|
+
// the world from the up-to-date listener pose.
|
|
11
|
+
//
|
|
12
|
+
// Per LOOM-AUDIO-SPEC.md §3.3 the work is one-line: read the local
|
|
13
|
+
// character's TransformPool entry by entity id, write its (x, y, z)
|
|
14
|
+
// into the AudioListenerResource.pose, stamp lastUpdateFrame, and
|
|
15
|
+
// hand the pose to SpatialAudioBus.setListener.
|
|
16
|
+
//
|
|
17
|
+
// Identity binding: the engine doesn't know which entity is the local
|
|
18
|
+
// character - that's a consumer concern (TWT wires it after server
|
|
19
|
+
// auth completes). Consumers call setLocalCharacterEntity(entity) on
|
|
20
|
+
// the system. setLocalCharacterEntity(null) puts the system back into
|
|
21
|
+
// no-op mode (e.g. between zones during a teleport).
|
|
22
|
+
//
|
|
23
|
+
// Tolerates:
|
|
24
|
+
// - missing AudioListenerResource (silent no-op; engine probably
|
|
25
|
+
// not yet fully wired)
|
|
26
|
+
// - missing SpatialAudioBus (silent no-op; consumer might have
|
|
27
|
+
// opted out of spatial audio entirely - the system still keeps
|
|
28
|
+
// the resource fresh for debug/HUD readers)
|
|
29
|
+
// - missing TransformPool (silent no-op; engine without ECS
|
|
30
|
+
// transforms is unusual but valid in headless server tests)
|
|
31
|
+
// - local character set but transform not attached yet (no-op)
|
|
32
|
+
//
|
|
33
|
+
// What this system does NOT do (deferred to future phases):
|
|
34
|
+
// - rotate the listener with the player's facing (spec §8.4 fixes
|
|
35
|
+
// forward = (0, 0, -1))
|
|
36
|
+
// - apply velocity-based Doppler (PannerNode.setVelocity is
|
|
37
|
+
// deprecated; v1 ships flat doppler-off audio)
|
|
38
|
+
// - any per-source position update (handle.setPosition owns that)
|
|
39
|
+
import { entityIndex } from '../entity.js';
|
|
40
|
+
import { POOL_TRANSFORM } from '../world.js';
|
|
41
|
+
import { RESOURCE_TIME, } from '../resources.js';
|
|
42
|
+
import { RESOURCE_AUDIO_LISTENER, } from './audio-listener-resource.js';
|
|
43
|
+
export class SpatialAudioSystem {
|
|
44
|
+
name = 'spatial-audio';
|
|
45
|
+
localCharacter = null;
|
|
46
|
+
spatialBus = null;
|
|
47
|
+
// Optional spatial bus reference. The system can also work without
|
|
48
|
+
// a bus (resource-only mode) - useful for tests and for consumers
|
|
49
|
+
// who do not want positional audio but still want a tracked listener
|
|
50
|
+
// pose (e.g. for visual SFX triggered relative to the player).
|
|
51
|
+
constructor(opts = {}) {
|
|
52
|
+
this.spatialBus = opts.spatialBus ?? null;
|
|
53
|
+
}
|
|
54
|
+
// Wire (or rewire) the spatial bus. Useful when the AudioBus is
|
|
55
|
+
// created lazily after first user gesture - the system can be
|
|
56
|
+
// registered upfront and the bus attached later.
|
|
57
|
+
setSpatialBus(bus) {
|
|
58
|
+
this.spatialBus = bus;
|
|
59
|
+
}
|
|
60
|
+
// Bind the local character entity. null means "no local character"
|
|
61
|
+
// (e.g. mid-teleport, character not yet allocated, headless mode).
|
|
62
|
+
// The system tolerates an entity that doesn't have a transform yet
|
|
63
|
+
// by silently skipping the update.
|
|
64
|
+
setLocalCharacterEntity(entity) {
|
|
65
|
+
this.localCharacter = entity;
|
|
66
|
+
}
|
|
67
|
+
getLocalCharacterEntity() {
|
|
68
|
+
return this.localCharacter;
|
|
69
|
+
}
|
|
70
|
+
update(world, _dt) {
|
|
71
|
+
if (this.localCharacter === null)
|
|
72
|
+
return;
|
|
73
|
+
var listener = world.resources.get(RESOURCE_AUDIO_LISTENER);
|
|
74
|
+
if (!listener)
|
|
75
|
+
return;
|
|
76
|
+
var transforms = world.getPool(POOL_TRANSFORM);
|
|
77
|
+
if (!transforms)
|
|
78
|
+
return;
|
|
79
|
+
var idx = entityIndex(this.localCharacter);
|
|
80
|
+
// Bounds check against the high-water mark so a fresh entity that
|
|
81
|
+
// the consumer registered before attaching a transform doesn't
|
|
82
|
+
// produce undefined reads on the typed arrays.
|
|
83
|
+
if (idx >= transforms.getHighWaterMark())
|
|
84
|
+
return;
|
|
85
|
+
var x = transforms.x[idx] ?? 0;
|
|
86
|
+
var y = transforms.y[idx] ?? 0;
|
|
87
|
+
var z = transforms.z[idx] ?? 0;
|
|
88
|
+
listener.pose.x = x;
|
|
89
|
+
listener.pose.y = y;
|
|
90
|
+
listener.pose.z = z;
|
|
91
|
+
var time = world.resources.get(RESOURCE_TIME);
|
|
92
|
+
listener.lastUpdateFrame = time ? time.frame : listener.lastUpdateFrame + 1;
|
|
93
|
+
if (this.spatialBus) {
|
|
94
|
+
this.spatialBus.setListener(listener.pose);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=spatial-audio-system.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spatial-audio-system.js","sourceRoot":"","sources":["../../src/audio/spatial-audio-system.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { System } from '../system.js';
|
|
2
|
+
import type { World } from '../world.js';
|
|
3
|
+
import type { ZoneEvent, ZoneEventType } from '../director/zone/zone-event-envelope.js';
|
|
4
|
+
export interface PositionalPlayOptionsStub {
|
|
5
|
+
x?: number;
|
|
6
|
+
y?: number;
|
|
7
|
+
z?: number;
|
|
8
|
+
gain?: number;
|
|
9
|
+
rate?: number;
|
|
10
|
+
loop?: boolean;
|
|
11
|
+
refDistance?: number;
|
|
12
|
+
maxDistance?: number;
|
|
13
|
+
rolloffFactor?: number;
|
|
14
|
+
distanceModel?: 'linear' | 'inverse' | 'exponential';
|
|
15
|
+
}
|
|
16
|
+
export interface AudioListenerPoseStub {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
z?: number;
|
|
20
|
+
forward?: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
z: number;
|
|
24
|
+
};
|
|
25
|
+
up?: {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
z: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface AudioListenerResourceStub {
|
|
32
|
+
pose: AudioListenerPoseStub;
|
|
33
|
+
lastUpdateFrame: number;
|
|
34
|
+
}
|
|
35
|
+
export interface CueCatalogStub {
|
|
36
|
+
play(name: string, options?: PositionalPlayOptionsStub): unknown;
|
|
37
|
+
}
|
|
38
|
+
export interface MusicDirectorStub {
|
|
39
|
+
playMusic(name: string, fadeInMs?: number): void;
|
|
40
|
+
stopMusic(fadeOutMs?: number): Promise<void> | void;
|
|
41
|
+
crossfadeMusic(name: string, fadeMs?: number): void;
|
|
42
|
+
currentMusic(): string | null;
|
|
43
|
+
}
|
|
44
|
+
export declare const RESOURCE_AUDIO_LISTENER_STUB = "audio_listener";
|
|
45
|
+
export declare const RESOURCE_CUE_CATALOG_STUB = "cue_catalog";
|
|
46
|
+
export declare const RESOURCE_MUSIC_DIRECTOR_STUB = "music_director";
|
|
47
|
+
export interface ZoneCuePlay {
|
|
48
|
+
cue: string;
|
|
49
|
+
options?: PositionalPlayOptionsStub;
|
|
50
|
+
}
|
|
51
|
+
export interface ZoneAudioContext {
|
|
52
|
+
cues: CueCatalogStub | null;
|
|
53
|
+
music: MusicDirectorStub | null;
|
|
54
|
+
localZone: string | null;
|
|
55
|
+
listener: AudioListenerPoseStub;
|
|
56
|
+
}
|
|
57
|
+
export interface ZoneAudioMapping {
|
|
58
|
+
eventType: ZoneEventType;
|
|
59
|
+
handle(event: ZoneEvent, ctx: ZoneAudioContext): ZoneCuePlay | null;
|
|
60
|
+
}
|
|
61
|
+
export interface ZoneAudioSystemOptions {
|
|
62
|
+
currentZone?: () => string | null;
|
|
63
|
+
verbose?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export declare class ZoneAudioSystem implements System {
|
|
66
|
+
readonly name: string;
|
|
67
|
+
private readonly mappings;
|
|
68
|
+
private readonly currentZone;
|
|
69
|
+
private readonly verbose;
|
|
70
|
+
private readonly lastProcessedIdByZone;
|
|
71
|
+
constructor(opts?: ZoneAudioSystemOptions);
|
|
72
|
+
registerMapping(mapping: ZoneAudioMapping): void;
|
|
73
|
+
unregisterMapping(eventType: ZoneEventType): void;
|
|
74
|
+
hasMapping(eventType: ZoneEventType): boolean;
|
|
75
|
+
mappingCount(): number;
|
|
76
|
+
update(world: World, _dt: number): void;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=zone-audio-system.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zone-audio-system.d.ts","sourceRoot":"","sources":["../../src/audio/zone-audio-system.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// ZoneAudioSystem - zone-event audio integration shell (Phase 17, Track C).
|
|
2
|
+
//
|
|
3
|
+
// Runs PHASE_RENDER, AFTER ZoneEventSystem (which appends every
|
|
4
|
+
// observed zone event to the per-zone ring buffer in
|
|
5
|
+
// ZoneEventLog.byZone[<zone>].recent). Each tick the audio system
|
|
6
|
+
// drains the local zone's ring buffer for events that landed THIS
|
|
7
|
+
// frame (i.e. events with id strictly greater than the last id we
|
|
8
|
+
// processed for that zone), looks up the consumer-registered mapping
|
|
9
|
+
// for each event type, and dispatches the resulting cue play through
|
|
10
|
+
// the registered CueCatalog / MusicDirector resources.
|
|
11
|
+
//
|
|
12
|
+
// This file ships ZERO mappings. Engine consumers (TheWorldTable.ai,
|
|
13
|
+
// etc.) register their own mappings for the cues they want to fire on
|
|
14
|
+
// each zone event type.
|
|
15
|
+
//
|
|
16
|
+
// Tolerances:
|
|
17
|
+
// - Missing mapping for an event type: log + skip silently. Most
|
|
18
|
+
// consumers will register only a subset of the seven zone event
|
|
19
|
+
// types.
|
|
20
|
+
// - Missing CueCatalog / MusicDirector resources: no-op gracefully.
|
|
21
|
+
// The system keeps draining events but cue plays evaporate. This
|
|
22
|
+
// lets a consumer wire ZoneAudioSystem before the catalog is built
|
|
23
|
+
// (e.g. while audio assets are still loading) without crashing.
|
|
24
|
+
// - Missing ZoneEventLog: no-op (nothing to drain).
|
|
25
|
+
// - Missing AudioListener resource: cue plays still dispatch but
|
|
26
|
+
// `listener` in the handler context falls back to a zero pose.
|
|
27
|
+
// - Multiple mappings for the SAME event type: registerMapping
|
|
28
|
+
// overwrites. unregisterMapping by eventType.
|
|
29
|
+
//
|
|
30
|
+
// Dispatch ORDER: registerMapping registration order is irrelevant
|
|
31
|
+
// since each event type has at most ONE handler. The order in which
|
|
32
|
+
// events are processed within a tick is the order they appear in
|
|
33
|
+
// ZoneEventLog.byZone[<zone>].recent (newest first, per Phase 16
|
|
34
|
+
// log semantics). Per spec we replay newest-first so a consumer that
|
|
35
|
+
// registers a music crossfade reacts to the latest knot pulse before
|
|
36
|
+
// older ones; the zone-events log is a deduped ring, so duplicates
|
|
37
|
+
// are not a concern.
|
|
38
|
+
import { RESOURCE_ZONE_EVENT_LOG, } from '../director/zone/zone-event-log.js';
|
|
39
|
+
// Resource keys (defined here as duplicate string constants to keep
|
|
40
|
+
// the system standalone). The values MATCH the ones declared in the
|
|
41
|
+
// Track A / Track B files; coordination merge will swap to a single
|
|
42
|
+
// import after 0.15.0 assembles.
|
|
43
|
+
// TODO[phase-17-merge]: replace these constants with re-imports from
|
|
44
|
+
// audio-listener-resource.ts / cue-catalog.ts / music-director.ts.
|
|
45
|
+
export const RESOURCE_AUDIO_LISTENER_STUB = 'audio_listener';
|
|
46
|
+
export const RESOURCE_CUE_CATALOG_STUB = 'cue_catalog';
|
|
47
|
+
export const RESOURCE_MUSIC_DIRECTOR_STUB = 'music_director';
|
|
48
|
+
// ---- The system ----
|
|
49
|
+
export class ZoneAudioSystem {
|
|
50
|
+
name = 'zone-audio';
|
|
51
|
+
mappings;
|
|
52
|
+
currentZone;
|
|
53
|
+
verbose;
|
|
54
|
+
// Per-zone last processed event id. Drives the "events landed this
|
|
55
|
+
// frame" filter so the system reads the per-zone ring buffer once
|
|
56
|
+
// per tick and fires only fresh events.
|
|
57
|
+
lastProcessedIdByZone;
|
|
58
|
+
constructor(opts = {}) {
|
|
59
|
+
this.mappings = new Map();
|
|
60
|
+
this.currentZone = opts.currentZone ?? null;
|
|
61
|
+
this.verbose = opts.verbose ?? false;
|
|
62
|
+
this.lastProcessedIdByZone = new Map();
|
|
63
|
+
}
|
|
64
|
+
registerMapping(mapping) {
|
|
65
|
+
if (!mapping || typeof mapping.eventType !== 'string'
|
|
66
|
+
|| typeof mapping.handle !== 'function') {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.mappings.set(mapping.eventType, mapping);
|
|
70
|
+
}
|
|
71
|
+
unregisterMapping(eventType) {
|
|
72
|
+
this.mappings.delete(eventType);
|
|
73
|
+
}
|
|
74
|
+
// For tests + introspection.
|
|
75
|
+
hasMapping(eventType) {
|
|
76
|
+
return this.mappings.has(eventType);
|
|
77
|
+
}
|
|
78
|
+
mappingCount() {
|
|
79
|
+
return this.mappings.size;
|
|
80
|
+
}
|
|
81
|
+
update(world, _dt) {
|
|
82
|
+
const log = world.resources.get(RESOURCE_ZONE_EVENT_LOG);
|
|
83
|
+
if (!log)
|
|
84
|
+
return;
|
|
85
|
+
const cues = world.resources.get(RESOURCE_CUE_CATALOG_STUB) ?? null;
|
|
86
|
+
const music = world.resources.get(RESOURCE_MUSIC_DIRECTOR_STUB) ?? null;
|
|
87
|
+
const listenerRes = world.resources.get(RESOURCE_AUDIO_LISTENER_STUB);
|
|
88
|
+
const listener = listenerRes
|
|
89
|
+
? listenerRes.pose
|
|
90
|
+
: { x: 0, y: 0, z: 0 };
|
|
91
|
+
const localZone = safeCurrentZone(this.currentZone);
|
|
92
|
+
// Decide which zones to drain. With a local-zone filter set, we
|
|
93
|
+
// drain ONLY that zone; without it, we drain every zone present
|
|
94
|
+
// in the log (single-zone consumers + tests).
|
|
95
|
+
const zonesToDrain = [];
|
|
96
|
+
if (localZone !== null) {
|
|
97
|
+
if (log.byZone.has(localZone))
|
|
98
|
+
zonesToDrain.push(localZone);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
for (const z of log.byZone.keys())
|
|
102
|
+
zonesToDrain.push(z);
|
|
103
|
+
}
|
|
104
|
+
for (let zi = 0; zi < zonesToDrain.length; zi++) {
|
|
105
|
+
const zone = zonesToDrain[zi];
|
|
106
|
+
const entry = log.byZone.get(zone);
|
|
107
|
+
if (!entry)
|
|
108
|
+
continue;
|
|
109
|
+
// recent[] is newest-first per Phase 16 ZoneEventLog convention.
|
|
110
|
+
// We walk from oldest -> newest among UNSEEN events so handlers
|
|
111
|
+
// see events in chronological order (e.g. boss.spawn before
|
|
112
|
+
// boss.tick). Determine the cutoff by lastProcessedId for this
|
|
113
|
+
// zone.
|
|
114
|
+
const lastId = this.lastProcessedIdByZone.get(zone) ?? 0;
|
|
115
|
+
let maxIdSeen = lastId;
|
|
116
|
+
// Walk newest->oldest, collect unseen, then reverse so dispatch
|
|
117
|
+
// is oldest->newest.
|
|
118
|
+
const fresh = [];
|
|
119
|
+
for (let i = 0; i < entry.recent.length; i++) {
|
|
120
|
+
const ev = entry.recent[i];
|
|
121
|
+
if (!ev)
|
|
122
|
+
continue;
|
|
123
|
+
if (ev.id > lastId) {
|
|
124
|
+
fresh.push(ev);
|
|
125
|
+
if (ev.id > maxIdSeen)
|
|
126
|
+
maxIdSeen = ev.id;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Past the boundary; older events have already been
|
|
130
|
+
// processed.
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (fresh.length === 0)
|
|
135
|
+
continue;
|
|
136
|
+
// Reverse to oldest-first.
|
|
137
|
+
for (let i = fresh.length - 1; i >= 0; i--) {
|
|
138
|
+
const ev = fresh[i];
|
|
139
|
+
const mapping = this.mappings.get(ev.type);
|
|
140
|
+
if (!mapping) {
|
|
141
|
+
if (this.verbose) {
|
|
142
|
+
try {
|
|
143
|
+
console.log('[zone-audio] no mapping for ' + ev.type + ' (zone='
|
|
144
|
+
+ ev.zone_id + ', id=' + ev.id + ')');
|
|
145
|
+
}
|
|
146
|
+
catch { /* ignore */ }
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const ctx = {
|
|
151
|
+
cues,
|
|
152
|
+
music,
|
|
153
|
+
localZone,
|
|
154
|
+
listener,
|
|
155
|
+
};
|
|
156
|
+
let result = null;
|
|
157
|
+
try {
|
|
158
|
+
result = mapping.handle(ev, ctx);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
if (this.verbose) {
|
|
162
|
+
try {
|
|
163
|
+
console.warn('[zone-audio] mapping handle threw', err);
|
|
164
|
+
}
|
|
165
|
+
catch { /* ignore */ }
|
|
166
|
+
}
|
|
167
|
+
result = null;
|
|
168
|
+
}
|
|
169
|
+
if (!result)
|
|
170
|
+
continue;
|
|
171
|
+
if (typeof result.cue !== 'string' || result.cue.length === 0)
|
|
172
|
+
continue;
|
|
173
|
+
if (!cues) {
|
|
174
|
+
// Catalog absent: silently drop the cue. The handler
|
|
175
|
+
// already ran (e.g. it called ctx.music.crossfadeMusic) so
|
|
176
|
+
// music side-effects still happen.
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
cues.play(result.cue, result.options);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
if (this.verbose) {
|
|
184
|
+
try {
|
|
185
|
+
console.warn('[zone-audio] cues.play threw', err);
|
|
186
|
+
}
|
|
187
|
+
catch { /* ignore */ }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.lastProcessedIdByZone.set(zone, maxIdSeen);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function safeCurrentZone(fn) {
|
|
196
|
+
if (!fn)
|
|
197
|
+
return null;
|
|
198
|
+
try {
|
|
199
|
+
const z = fn();
|
|
200
|
+
return typeof z === 'string' && z.length > 0 ? z : null;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=zone-audio-system.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zone-audio-system.js","sourceRoot":"","sources":["../../src/audio/zone-audio-system.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IAIPlugin, EmittedEvents, PluginContext, PeerInfo, PlayerAction } from './plugin.js';
|
|
2
|
+
export declare class AIPluginDuplicateError extends Error {
|
|
3
|
+
readonly pluginName: string;
|
|
4
|
+
constructor(pluginName: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class AIPluginRegistry {
|
|
7
|
+
private plugins;
|
|
8
|
+
register(plugin: IAIPlugin): void;
|
|
9
|
+
unregister(name: string): Promise<boolean>;
|
|
10
|
+
list(): ReadonlyArray<IAIPlugin>;
|
|
11
|
+
get(name: string): IAIPlugin | undefined;
|
|
12
|
+
dispatchTick(ctx: PluginContext): Promise<EmittedEvents>;
|
|
13
|
+
dispatchPeerJoin(ctx: PluginContext, peer: PeerInfo): Promise<EmittedEvents>;
|
|
14
|
+
dispatchPeerLeave(ctx: PluginContext, peer: PeerInfo): Promise<EmittedEvents>;
|
|
15
|
+
dispatchZoneEnter(ctx: PluginContext, peer: PeerInfo, fromZone: string | null): Promise<EmittedEvents>;
|
|
16
|
+
dispatchPlayerAction(ctx: PluginContext, peer: PeerInfo, action: PlayerAction): Promise<EmittedEvents>;
|
|
17
|
+
private dispatch;
|
|
18
|
+
private errorMeta;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=ai-plugin-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-plugin-registry.d.ts","sourceRoot":"","sources":["../../../src/director/ai/ai-plugin-registry.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,QAAQ,EACR,YAAY,EACb,MAAM,aAAa,CAAC;AAErB,qBAAa,sBAAuB,SAAQ,KAAK;aACnB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,gBAAgB;IAK3B,OAAO,CAAC,OAAO,CAAmB;IAOlC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IA0B3B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA8BhD,IAAI,IAAI,aAAa,CAAC,SAAS,CAAC;IAMhC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAgBlC,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAMxD,gBAAgB,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,CAAC;IAM5E,iBAAiB,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,CAAC;IAM7E,iBAAiB,CACrB,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,aAAa,CAAC;IAMnB,oBAAoB,CACxB,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,aAAa,CAAC;YAYX,QAAQ;IAiEtB,OAAO,CAAC,SAAS;CAgBlB"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// AIPluginRegistry - dispatches lifecycle hooks across registered plugins.
|
|
2
|
+
//
|
|
3
|
+
// Per LOOM-DIRECTOR-PROTOCOL-V2 Section 5.3: the registry holds N
|
|
4
|
+
// plugins, dispatches each hook in priority order (lower runs first),
|
|
5
|
+
// and concatenates the EmittedEvents from every plugin into a single
|
|
6
|
+
// merged result. The caller (the engine's emit path / TWT's loom_director
|
|
7
|
+
// orchestrator) takes the merged events and routes them - character
|
|
8
|
+
// events to the v1 stream, zone events to the v2 zone log + presence
|
|
9
|
+
// fanout.
|
|
10
|
+
//
|
|
11
|
+
// Error isolation guarantee (open question 8.3): if a plugin's hook
|
|
12
|
+
// throws synchronously OR the returned Promise rejects, the registry
|
|
13
|
+
// logs the failure via that plugin's logger, drops that plugin's
|
|
14
|
+
// contribution for THIS dispatch only, and continues with the next
|
|
15
|
+
// plugin. The dispatch never throws to the caller. Plugin authors
|
|
16
|
+
// remain responsible for their own internal try/catch; the registry
|
|
17
|
+
// is the safety net of last resort.
|
|
18
|
+
//
|
|
19
|
+
// Implementation notes:
|
|
20
|
+
// - Plugins are kept in a sorted array; register/unregister keep
|
|
21
|
+
// it sorted so the dispatch hot path is a simple iteration.
|
|
22
|
+
// - The registry never instantiates a PluginContext; the caller
|
|
23
|
+
// supplies one per dispatch (or reuses one and refreshes views).
|
|
24
|
+
// This keeps the registry decoupled from world-state plumbing.
|
|
25
|
+
// - dispose() is awaited inside unregister() so plugins can flush
|
|
26
|
+
// pending work; if dispose throws we log and continue (drop
|
|
27
|
+
// guarantee extends to dispose so the caller never sees plugin
|
|
28
|
+
// errors leak).
|
|
29
|
+
export class AIPluginDuplicateError extends Error {
|
|
30
|
+
pluginName;
|
|
31
|
+
constructor(pluginName) {
|
|
32
|
+
super('AIPluginDuplicateError: plugin already registered: ' + pluginName);
|
|
33
|
+
this.pluginName = pluginName;
|
|
34
|
+
this.name = 'AIPluginDuplicateError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class AIPluginRegistry {
|
|
38
|
+
// Sorted by priority ascending, then by registration order. The
|
|
39
|
+
// registration index is encoded by inserting at the right position
|
|
40
|
+
// on register() rather than re-sorting, which preserves stable
|
|
41
|
+
// ordering for equal priorities.
|
|
42
|
+
plugins = [];
|
|
43
|
+
// ----- Lifecycle -----
|
|
44
|
+
// Register a plugin. Throws AIPluginDuplicateError if a plugin with
|
|
45
|
+
// the same name is already registered (names are the registry key).
|
|
46
|
+
// Insertion is O(n) but n is small (typically 1-10 plugins).
|
|
47
|
+
register(plugin) {
|
|
48
|
+
for (var i = 0; i < this.plugins.length; i++) {
|
|
49
|
+
var existing = this.plugins[i];
|
|
50
|
+
if (existing && existing.name === plugin.name) {
|
|
51
|
+
throw new AIPluginDuplicateError(plugin.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Find the first plugin with strictly higher priority and insert
|
|
55
|
+
// before it. This preserves stable ordering for equal priorities
|
|
56
|
+
// (a later-registered plugin runs after an earlier one with the
|
|
57
|
+
// same priority).
|
|
58
|
+
var insertAt = this.plugins.length;
|
|
59
|
+
for (var j = 0; j < this.plugins.length; j++) {
|
|
60
|
+
var p = this.plugins[j];
|
|
61
|
+
if (p && p.priority > plugin.priority) {
|
|
62
|
+
insertAt = j;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.plugins.splice(insertAt, 0, plugin);
|
|
67
|
+
}
|
|
68
|
+
// Unregister a plugin by name. Awaits the plugin's dispose() if
|
|
69
|
+
// present; logs and drops if dispose throws. Returns true if a
|
|
70
|
+
// plugin was removed, false if no plugin with that name was
|
|
71
|
+
// registered.
|
|
72
|
+
async unregister(name) {
|
|
73
|
+
var idx = -1;
|
|
74
|
+
for (var i = 0; i < this.plugins.length; i++) {
|
|
75
|
+
var p = this.plugins[i];
|
|
76
|
+
if (p && p.name === name) {
|
|
77
|
+
idx = i;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (idx === -1)
|
|
82
|
+
return false;
|
|
83
|
+
var plugin = this.plugins[idx];
|
|
84
|
+
this.plugins.splice(idx, 1);
|
|
85
|
+
if (plugin && typeof plugin.dispose === 'function') {
|
|
86
|
+
try {
|
|
87
|
+
await plugin.dispose();
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// Drop guarantee extends to dispose. We can't log via the
|
|
91
|
+
// plugin's logger because we have no PluginContext here;
|
|
92
|
+
// fall back to console with the plugin name tag.
|
|
93
|
+
console.error('[plugin: ' + plugin.name + '] dispose() threw', this.errorMeta(err));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
// Read-only snapshot of the plugin list, in dispatch order. Returns
|
|
99
|
+
// a fresh array; mutating it does not affect the registry.
|
|
100
|
+
list() {
|
|
101
|
+
return this.plugins.slice();
|
|
102
|
+
}
|
|
103
|
+
// Look up a plugin by name. Returns undefined if no plugin with
|
|
104
|
+
// that name is registered.
|
|
105
|
+
get(name) {
|
|
106
|
+
for (var i = 0; i < this.plugins.length; i++) {
|
|
107
|
+
var p = this.plugins[i];
|
|
108
|
+
if (p && p.name === name)
|
|
109
|
+
return p;
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
// ----- Dispatchers -----
|
|
114
|
+
// All five dispatchers share the same shape: iterate plugins in
|
|
115
|
+
// priority order, call the hook if defined, await the result, merge
|
|
116
|
+
// into the running EmittedEvents. Errors are caught per-plugin and
|
|
117
|
+
// logged via the plugin's logger if reachable through ctx; never
|
|
118
|
+
// thrown to the caller.
|
|
119
|
+
async dispatchTick(ctx) {
|
|
120
|
+
return this.dispatch(ctx, 'onTick', function (plugin, ctx) {
|
|
121
|
+
return plugin.onTick(ctx);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async dispatchPeerJoin(ctx, peer) {
|
|
125
|
+
return this.dispatch(ctx, 'onPeerJoin', function (plugin, ctx) {
|
|
126
|
+
return plugin.onPeerJoin(ctx, peer);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async dispatchPeerLeave(ctx, peer) {
|
|
130
|
+
return this.dispatch(ctx, 'onPeerLeave', function (plugin, ctx) {
|
|
131
|
+
return plugin.onPeerLeave(ctx, peer);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async dispatchZoneEnter(ctx, peer, fromZone) {
|
|
135
|
+
return this.dispatch(ctx, 'onZoneEnter', function (plugin, ctx) {
|
|
136
|
+
return plugin.onZoneEnter(ctx, peer, fromZone);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async dispatchPlayerAction(ctx, peer, action) {
|
|
140
|
+
return this.dispatch(ctx, 'onPlayerAction', function (plugin, ctx) {
|
|
141
|
+
return plugin.onPlayerAction(ctx, peer, action);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// ----- Internals -----
|
|
145
|
+
// Generic dispatch helper. `hookName` is the property name we check
|
|
146
|
+
// for definition on each plugin; `invoke` calls the actual hook
|
|
147
|
+
// (the spread of args differs per dispatcher). Per-plugin try/catch
|
|
148
|
+
// around the await isolates failures.
|
|
149
|
+
async dispatch(ctx, hookName, invoke) {
|
|
150
|
+
var merged = {};
|
|
151
|
+
var characterEvents = undefined;
|
|
152
|
+
var zoneEvents = undefined;
|
|
153
|
+
// Snapshot the plugin list at dispatch start so a mutation during
|
|
154
|
+
// the dispatch (a hook calls registry.register/unregister) cannot
|
|
155
|
+
// change which plugins run for THIS dispatch. Spec doesn't forbid
|
|
156
|
+
// mutation but the predictable behavior is "this dispatch sees
|
|
157
|
+
// the registry as it was at start".
|
|
158
|
+
var snapshot = this.plugins.slice();
|
|
159
|
+
for (var i = 0; i < snapshot.length; i++) {
|
|
160
|
+
var plugin = snapshot[i];
|
|
161
|
+
if (!plugin)
|
|
162
|
+
continue;
|
|
163
|
+
// Hook not implemented by this plugin? Skip without allocating
|
|
164
|
+
// anything. Use bracket access so the keyof typing carries through.
|
|
165
|
+
var hook = plugin[hookName];
|
|
166
|
+
if (typeof hook !== 'function')
|
|
167
|
+
continue;
|
|
168
|
+
var emitted;
|
|
169
|
+
try {
|
|
170
|
+
emitted = await invoke(plugin, ctx);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
// Log via the plugin's logger so the failure is tagged with
|
|
174
|
+
// the plugin name. ctx.logger may itself throw; if so, fall
|
|
175
|
+
// back to console (cannot let logger errors break dispatch).
|
|
176
|
+
try {
|
|
177
|
+
ctx.logger.error('plugin hook ' + String(hookName) + ' threw', this.errorMeta(err));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
console.error('[plugin: ' + plugin.name + '] hook ' + String(hookName) + ' threw', this.errorMeta(err));
|
|
181
|
+
}
|
|
182
|
+
// Drop this plugin's contribution for this dispatch.
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (!emitted)
|
|
186
|
+
continue;
|
|
187
|
+
if (emitted.characterEvents && emitted.characterEvents.length > 0) {
|
|
188
|
+
if (!characterEvents)
|
|
189
|
+
characterEvents = [];
|
|
190
|
+
for (var ci = 0; ci < emitted.characterEvents.length; ci++) {
|
|
191
|
+
var ce = emitted.characterEvents[ci];
|
|
192
|
+
if (ce)
|
|
193
|
+
characterEvents.push(ce);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (emitted.zoneEvents && emitted.zoneEvents.length > 0) {
|
|
197
|
+
if (!zoneEvents)
|
|
198
|
+
zoneEvents = [];
|
|
199
|
+
for (var zi = 0; zi < emitted.zoneEvents.length; zi++) {
|
|
200
|
+
var ze = emitted.zoneEvents[zi];
|
|
201
|
+
if (ze)
|
|
202
|
+
zoneEvents.push(ze);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (characterEvents)
|
|
207
|
+
merged.characterEvents = characterEvents;
|
|
208
|
+
if (zoneEvents)
|
|
209
|
+
merged.zoneEvents = zoneEvents;
|
|
210
|
+
return merged;
|
|
211
|
+
}
|
|
212
|
+
// Pull useful fields out of an unknown error for log meta. Avoids
|
|
213
|
+
// throwing on non-Error throws (strings, objects, undefined).
|
|
214
|
+
errorMeta(err) {
|
|
215
|
+
if (err instanceof Error) {
|
|
216
|
+
return {
|
|
217
|
+
error_name: err.name,
|
|
218
|
+
error_message: err.message,
|
|
219
|
+
error_stack: err.stack ?? null,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
var safe;
|
|
223
|
+
try {
|
|
224
|
+
safe = JSON.stringify(err);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
safe = String(err);
|
|
228
|
+
}
|
|
229
|
+
return { error: safe };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=ai-plugin-registry.js.map
|