@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,40 @@
|
|
|
1
|
+
import type { ZoneEvent } from './zone-event-envelope.js';
|
|
2
|
+
import { type IZoneEventBridge, type ZoneEventBridgeStatus, type ZoneEventBridgeStats } from './zone-event-bridge.js';
|
|
3
|
+
export interface SSEZoneBridgeEventSource {
|
|
4
|
+
readonly readyState: number;
|
|
5
|
+
addEventListener(type: string, listener: (event: {
|
|
6
|
+
data?: unknown;
|
|
7
|
+
}) => void): void;
|
|
8
|
+
removeEventListener?(type: string, listener: (event: {
|
|
9
|
+
data?: unknown;
|
|
10
|
+
}) => void): void;
|
|
11
|
+
}
|
|
12
|
+
export interface SSEZoneBridgeOptions {
|
|
13
|
+
eventSource: SSEZoneBridgeEventSource;
|
|
14
|
+
characterId: string;
|
|
15
|
+
currentZone: () => string;
|
|
16
|
+
eventName?: string;
|
|
17
|
+
filterAtReceive?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare class SSEZoneBridge implements IZoneEventBridge {
|
|
20
|
+
private readonly es;
|
|
21
|
+
private readonly characterId;
|
|
22
|
+
private readonly currentZone;
|
|
23
|
+
private readonly eventName;
|
|
24
|
+
private readonly filterAtReceive;
|
|
25
|
+
private listener;
|
|
26
|
+
private queue;
|
|
27
|
+
private statusValue;
|
|
28
|
+
private readonly lastEventIdByZone;
|
|
29
|
+
private readonly statsValue;
|
|
30
|
+
constructor(opts: SSEZoneBridgeOptions);
|
|
31
|
+
start(): void;
|
|
32
|
+
stop(): void;
|
|
33
|
+
status(): ZoneEventBridgeStatus;
|
|
34
|
+
isConnected(): boolean;
|
|
35
|
+
getLastEventId(zone: string): number;
|
|
36
|
+
pollEvents(): ZoneEvent[];
|
|
37
|
+
stats(): Readonly<ZoneEventBridgeStats>;
|
|
38
|
+
private handleRaw;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=sse-zone-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-zone-bridge.d.ts","sourceRoot":"","sources":["../../../src/director/zone/sse-zone-bridge.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EAC1B,MAAM,wBAAwB,CAAC;AAIhC,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC5C,IAAI,CAAC;IACR,mBAAmB,CAAC,CAClB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC5C,IAAI,CAAC;CAGT;AAED,MAAM,WAAW,oBAAoB;IAInC,WAAW,EAAE,wBAAwB,CAAC;IAItC,WAAW,EAAE,MAAM,CAAC;IAMpB,WAAW,EAAE,MAAM,MAAM,CAAC;IAG1B,SAAS,CAAC,EAAE,MAAM,CAAC;IAInB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAOD,qBAAa,aAAc,YAAW,gBAAgB;IACpD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAA2B;IAC9C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAe;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;IAE1C,OAAO,CAAC,QAAQ,CAAsD;IACtE,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAkC;IACpE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAYzB;gBAEU,IAAI,EAAE,oBAAoB;IAWtC,KAAK,IAAI,IAAI;IAeb,IAAI,IAAI,IAAI;IAQZ,MAAM,IAAI,qBAAqB;IAe/B,WAAW,IAAI,OAAO;IAItB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAIpC,UAAU,IAAI,SAAS,EAAE;IAOzB,KAAK,IAAI,QAAQ,CAAC,oBAAoB,CAAC;IAavC,OAAO,CAAC,SAAS;CA6BlB"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// SSEZoneBridge - multiplexes v2 zone-event frames onto an EXISTING
|
|
2
|
+
// presence EventSource (per LOOM-DIRECTOR-PROTOCOL-V2 §2 + §4.2).
|
|
3
|
+
//
|
|
4
|
+
// CRITICAL: this bridge does NOT open its own EventSource. The
|
|
5
|
+
// presence transport (15.x SSEMultiplayerBridge) already has a live
|
|
6
|
+
// connection per peer; we just attach a listener for SSE frames whose
|
|
7
|
+
// `event:` line is `zone.event`. Per spec §1.1: "Transport reuses
|
|
8
|
+
// presence SSE. Same channel as 15.x presence updates, multiplexed by
|
|
9
|
+
// event topic. No second EventSource per peer."
|
|
10
|
+
//
|
|
11
|
+
// Wire shape per frame:
|
|
12
|
+
// event: zone.event
|
|
13
|
+
// data: <ZoneEventEnvelope JSON>
|
|
14
|
+
//
|
|
15
|
+
// Local-zone filter: the system layer (ZoneEventSystem) is the
|
|
16
|
+
// authoritative filter on "is this event for the local player's
|
|
17
|
+
// zone". The bridge buffers ALL zone events it receives so the system
|
|
18
|
+
// can log other-zone events for diagnostics if it chooses, then drop
|
|
19
|
+
// them before applying. The currentZone() callback is provided so the
|
|
20
|
+
// bridge can additionally short-circuit obvious-other-zone events at
|
|
21
|
+
// receive time when stats matter (e.g. a high-volume foreign zone).
|
|
22
|
+
// The default behaviour is to keep everything and let the system
|
|
23
|
+
// decide.
|
|
24
|
+
//
|
|
25
|
+
// Browser-only (constructor accepts EventSource which is a DOM API).
|
|
26
|
+
// In Node tests we exercise this via an injected fake EventSource
|
|
27
|
+
// (pattern parallel to SSEDirectorBridge.eventSourceFactory).
|
|
28
|
+
import { parseZoneEnvelopeJson } from './zone-event-envelope.js';
|
|
29
|
+
// SSE EventSource readyState constants. Mirrored locally to avoid
|
|
30
|
+
// pulling DOM types into the engine's strict Node test config.
|
|
31
|
+
const ES_OPEN = 1;
|
|
32
|
+
const ES_CLOSED = 2;
|
|
33
|
+
export class SSEZoneBridge {
|
|
34
|
+
es;
|
|
35
|
+
characterId;
|
|
36
|
+
currentZone;
|
|
37
|
+
eventName;
|
|
38
|
+
filterAtReceive;
|
|
39
|
+
listener = null;
|
|
40
|
+
queue = [];
|
|
41
|
+
statusValue = 'idle';
|
|
42
|
+
lastEventIdByZone = new Map();
|
|
43
|
+
statsValue = {
|
|
44
|
+
eventsReceived: 0,
|
|
45
|
+
reconnects: 0,
|
|
46
|
+
outOfOrderEvents: 0,
|
|
47
|
+
serverDropsP1: 0,
|
|
48
|
+
serverDropsP2: 0,
|
|
49
|
+
};
|
|
50
|
+
constructor(opts) {
|
|
51
|
+
this.es = opts.eventSource;
|
|
52
|
+
this.characterId = opts.characterId;
|
|
53
|
+
this.currentZone = opts.currentZone;
|
|
54
|
+
this.eventName = opts.eventName ?? 'zone.event';
|
|
55
|
+
this.filterAtReceive = opts.filterAtReceive ?? false;
|
|
56
|
+
// characterId is currently used for diagnostics only - reserved
|
|
57
|
+
// for future emitter_id-based UX hooks (e.g. local-cause cues).
|
|
58
|
+
void this.characterId;
|
|
59
|
+
}
|
|
60
|
+
start() {
|
|
61
|
+
if (this.listener)
|
|
62
|
+
return;
|
|
63
|
+
this.listener = (e) => { this.handleRaw(e); };
|
|
64
|
+
this.es.addEventListener(this.eventName, this.listener);
|
|
65
|
+
// The presence EventSource owns connect lifecycle. We mirror its
|
|
66
|
+
// current readyState so consumers can reason about isConnected().
|
|
67
|
+
if (this.es.readyState === ES_OPEN) {
|
|
68
|
+
this.statusValue = 'connected';
|
|
69
|
+
}
|
|
70
|
+
else if (this.es.readyState === ES_CLOSED) {
|
|
71
|
+
this.statusValue = 'closed';
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.statusValue = 'connecting';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
stop() {
|
|
78
|
+
this.statusValue = 'closed';
|
|
79
|
+
if (this.listener && this.es.removeEventListener) {
|
|
80
|
+
this.es.removeEventListener(this.eventName, this.listener);
|
|
81
|
+
}
|
|
82
|
+
this.listener = null;
|
|
83
|
+
}
|
|
84
|
+
status() {
|
|
85
|
+
// Refresh from the underlying ES if we're attached - the presence
|
|
86
|
+
// layer handles reconnects, so our cached status can lag.
|
|
87
|
+
if (this.listener) {
|
|
88
|
+
if (this.es.readyState === ES_OPEN) {
|
|
89
|
+
if (this.statusValue === 'connecting' || this.statusValue === 'reconnecting') {
|
|
90
|
+
this.statusValue = 'connected';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (this.es.readyState === ES_CLOSED) {
|
|
94
|
+
this.statusValue = 'closed';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return this.statusValue;
|
|
98
|
+
}
|
|
99
|
+
isConnected() {
|
|
100
|
+
return this.status() === 'connected';
|
|
101
|
+
}
|
|
102
|
+
getLastEventId(zone) {
|
|
103
|
+
return this.lastEventIdByZone.get(zone) ?? 0;
|
|
104
|
+
}
|
|
105
|
+
pollEvents() {
|
|
106
|
+
if (this.queue.length === 0)
|
|
107
|
+
return [];
|
|
108
|
+
const out = this.queue;
|
|
109
|
+
this.queue = [];
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
stats() {
|
|
113
|
+
return {
|
|
114
|
+
eventsReceived: this.statsValue.eventsReceived,
|
|
115
|
+
reconnects: this.statsValue.reconnects,
|
|
116
|
+
outOfOrderEvents: this.statsValue.outOfOrderEvents,
|
|
117
|
+
serverDropsP1: this.statsValue.serverDropsP1,
|
|
118
|
+
serverDropsP2: this.statsValue.serverDropsP2,
|
|
119
|
+
lastEventIdByZone: new Map(this.lastEventIdByZone),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// ----- Internal -----
|
|
123
|
+
handleRaw(e) {
|
|
124
|
+
const dataStr = typeof e.data === 'string' ? e.data : '';
|
|
125
|
+
const ev = parseZoneEnvelopeJson(dataStr);
|
|
126
|
+
if (!ev) {
|
|
127
|
+
// Malformed envelope - drop. The presence layer's EventSource
|
|
128
|
+
// keeps flowing; one bad frame doesn't kill the channel.
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (this.filterAtReceive) {
|
|
132
|
+
// Optional optimization: drop foreign-zone events before they
|
|
133
|
+
// hit the queue. The system would have done it anyway.
|
|
134
|
+
const localZone = safeCurrentZone(this.currentZone);
|
|
135
|
+
if (localZone && ev.zone_id !== localZone) {
|
|
136
|
+
// Still track the highest id for that foreign zone so the
|
|
137
|
+
// bridge's per-zone last-id map is honest about what arrived.
|
|
138
|
+
const prev = this.lastEventIdByZone.get(ev.zone_id) ?? 0;
|
|
139
|
+
if (ev.id > prev)
|
|
140
|
+
this.lastEventIdByZone.set(ev.zone_id, ev.id);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.statsValue.eventsReceived++;
|
|
145
|
+
const prev = this.lastEventIdByZone.get(ev.zone_id) ?? 0;
|
|
146
|
+
if (ev.id > prev) {
|
|
147
|
+
this.lastEventIdByZone.set(ev.zone_id, ev.id);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
this.statsValue.outOfOrderEvents++;
|
|
151
|
+
}
|
|
152
|
+
this.queue.push(ev);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function safeCurrentZone(fn) {
|
|
156
|
+
try {
|
|
157
|
+
const z = fn();
|
|
158
|
+
return typeof z === 'string' && z.length > 0 ? z : null;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=sse-zone-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-zone-bridge.js","sourceRoot":"","sources":["../../../src/director/zone/sse-zone-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ZoneEvent } from './zone-event-envelope.js';
|
|
2
|
+
export type ZoneEventBridgeStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'snapshot-required' | 'closed';
|
|
3
|
+
export interface ZoneEventBridgeStats {
|
|
4
|
+
eventsReceived: number;
|
|
5
|
+
reconnects: number;
|
|
6
|
+
outOfOrderEvents: number;
|
|
7
|
+
serverDropsP1: number;
|
|
8
|
+
serverDropsP2: number;
|
|
9
|
+
lastEventIdByZone: ReadonlyMap<string, number>;
|
|
10
|
+
}
|
|
11
|
+
export interface IZoneEventBridge {
|
|
12
|
+
start(): void;
|
|
13
|
+
stop(): void;
|
|
14
|
+
status(): ZoneEventBridgeStatus;
|
|
15
|
+
isConnected(): boolean;
|
|
16
|
+
getLastEventId(zone: string): number;
|
|
17
|
+
pollEvents(): ZoneEvent[];
|
|
18
|
+
stats(): Readonly<ZoneEventBridgeStats>;
|
|
19
|
+
}
|
|
20
|
+
export declare const RESOURCE_ZONE_EVENT_BRIDGE = "zone_event_bridge";
|
|
21
|
+
//# sourceMappingURL=zone-event-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zone-event-bridge.d.ts","sourceRoot":"","sources":["../../../src/director/zone/zone-event-bridge.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,MAAM,MAAM,qBAAqB,GAC7B,MAAM,GACN,YAAY,GACZ,WAAW,GACX,cAAc,GACd,mBAAmB,GACnB,QAAQ,CAAC;AAEb,MAAM,WAAW,oBAAoB;IAEnC,cAAc,EAAE,MAAM,CAAC;IAEvB,UAAU,EAAE,MAAM,CAAC;IAEnB,gBAAgB,EAAE,MAAM,CAAC;IAGzB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IAGtB,iBAAiB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;IACb,MAAM,IAAI,qBAAqB,CAAC;IAChC,WAAW,IAAI,OAAO,CAAC;IAEvB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAErC,UAAU,IAAI,SAAS,EAAE,CAAC;IAC1B,KAAK,IAAI,QAAQ,CAAC,oBAAoB,CAAC,CAAC;CACzC;AAGD,eAAO,MAAM,0BAA0B,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// IZoneEventBridge - abstraction over the v2 zone-event source.
|
|
2
|
+
//
|
|
3
|
+
// Concrete implementations:
|
|
4
|
+
// MockZoneBridge - in-process. enqueueIncoming(event) simulates
|
|
5
|
+
// server pushes for tests + offline demo.
|
|
6
|
+
// SSEZoneBridge - multiplexes onto an EXISTING presence
|
|
7
|
+
// EventSource. Listens for SSE frames whose
|
|
8
|
+
// `event:` line is `zone.event` (per spec §2.1).
|
|
9
|
+
//
|
|
10
|
+
// The ZoneEventSystem (PHASE_INPUT, AFTER DirectorSystem and
|
|
11
|
+
// PeerPresenceSystem) calls pollEvents() once per tick to drain queued
|
|
12
|
+
// events. Bridges buffer events between polls so the system's per-tick
|
|
13
|
+
// read is bounded.
|
|
14
|
+
//
|
|
15
|
+
// Per-zone monotonic id semantics (spec §8.1):
|
|
16
|
+
// - Each zone has its own id sequence starting at 1.
|
|
17
|
+
// - getLastEventId(zone) returns the highest id observed for that
|
|
18
|
+
// zone, or 0 if no events seen.
|
|
19
|
+
// - Out-of-order events are tracked in stats, but bridge-level
|
|
20
|
+
// reorder buffering lives in concrete impls (the contract here is
|
|
21
|
+
// the polled queue is best-effort ordered).
|
|
22
|
+
// Resource keys.
|
|
23
|
+
export const RESOURCE_ZONE_EVENT_BRIDGE = 'zone_event_bridge';
|
|
24
|
+
//# sourceMappingURL=zone-event-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zone-event-bridge.js","sourceRoot":"","sources":["../../../src/director/zone/zone-event-bridge.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,EAAE;AACF,4BAA4B;AAC5B,kEAAkE;AAClE,6DAA6D;AAC7D,2DAA2D;AAC3D,+DAA+D;AAC/D,oEAAoE;AACpE,EAAE;AACF,6DAA6D;AAC7D,uEAAuE;AACvE,uEAAuE;AACvE,mBAAmB;AACnB,EAAE;AACF,+CAA+C;AAC/C,uDAAuD;AACvD,oEAAoE;AACpE,oCAAoC;AACpC,iEAAiE;AACjE,sEAAsE;AACtE,gDAAgD;AAwChD,iBAAiB;AACjB,MAAM,CAAC,MAAM,0BAA0B,GAAG,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { DropSpec, EventPriority, KnotMood, KnotPaletteHex, NarratorVoice } from '../event-envelope.js';
|
|
2
|
+
export interface ZoneEventEnvelope<T extends ZoneEventType = ZoneEventType> {
|
|
3
|
+
id: number;
|
|
4
|
+
ts: number;
|
|
5
|
+
type: T;
|
|
6
|
+
zone_id: string;
|
|
7
|
+
emitter_id: string | null;
|
|
8
|
+
priority?: EventPriority;
|
|
9
|
+
data: ZoneEventDataMap[T];
|
|
10
|
+
}
|
|
11
|
+
export interface ZoneBossSpec {
|
|
12
|
+
boss_id: string;
|
|
13
|
+
type: string;
|
|
14
|
+
name: string;
|
|
15
|
+
hp_max: number;
|
|
16
|
+
hp_current: number;
|
|
17
|
+
dmg: number;
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
knot_flavor: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ZoneBossSpawnData {
|
|
23
|
+
boss: ZoneBossSpec;
|
|
24
|
+
narrator_line: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface ZoneBossHit {
|
|
27
|
+
from_character_id: string;
|
|
28
|
+
amount: number;
|
|
29
|
+
ts_ms: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ZoneBossTickData {
|
|
32
|
+
boss_id: string;
|
|
33
|
+
hp_current: number;
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
recent_hits: ReadonlyArray<ZoneBossHit>;
|
|
37
|
+
}
|
|
38
|
+
export type ZoneBossOutcome = 'killed' | 'despawned' | 'fled';
|
|
39
|
+
export interface ZoneBossEndData {
|
|
40
|
+
boss_id: string;
|
|
41
|
+
outcome: ZoneBossOutcome;
|
|
42
|
+
killer_character_id: string | null;
|
|
43
|
+
loot: ReadonlyArray<DropSpec>;
|
|
44
|
+
duration_ms: number;
|
|
45
|
+
}
|
|
46
|
+
export interface ZoneNarratorData {
|
|
47
|
+
line: string;
|
|
48
|
+
voice: NarratorVoice;
|
|
49
|
+
ttl_ms: number;
|
|
50
|
+
}
|
|
51
|
+
export interface ZoneKnotData {
|
|
52
|
+
knot: string;
|
|
53
|
+
palette: KnotPaletteHex;
|
|
54
|
+
mood: KnotMood;
|
|
55
|
+
fade_ms: number;
|
|
56
|
+
}
|
|
57
|
+
export interface ZoneStateChange {
|
|
58
|
+
key: string;
|
|
59
|
+
value: unknown;
|
|
60
|
+
}
|
|
61
|
+
export interface ZoneStateData {
|
|
62
|
+
changes: ReadonlyArray<ZoneStateChange>;
|
|
63
|
+
}
|
|
64
|
+
export interface ZoneSnapshotData {
|
|
65
|
+
active_boss: ZoneBossSpec | null;
|
|
66
|
+
knot: ZoneKnotData | null;
|
|
67
|
+
state: ReadonlyArray<ZoneStateChange>;
|
|
68
|
+
last_event_id: number;
|
|
69
|
+
}
|
|
70
|
+
export type ZoneEventType = 'zone.boss.spawn' | 'zone.boss.tick' | 'zone.boss.end' | 'zone.narrator' | 'zone.knot' | 'zone.state' | 'zone.snapshot';
|
|
71
|
+
export interface ZoneEventDataMap {
|
|
72
|
+
'zone.boss.spawn': ZoneBossSpawnData;
|
|
73
|
+
'zone.boss.tick': ZoneBossTickData;
|
|
74
|
+
'zone.boss.end': ZoneBossEndData;
|
|
75
|
+
'zone.narrator': ZoneNarratorData;
|
|
76
|
+
'zone.knot': ZoneKnotData;
|
|
77
|
+
'zone.state': ZoneStateData;
|
|
78
|
+
'zone.snapshot': ZoneSnapshotData;
|
|
79
|
+
}
|
|
80
|
+
export type ZoneEvent = {
|
|
81
|
+
[K in ZoneEventType]: ZoneEventEnvelope<K>;
|
|
82
|
+
}[ZoneEventType];
|
|
83
|
+
export declare function priorityFor(type: ZoneEventType): EventPriority;
|
|
84
|
+
export declare class ZoneEventEnvelopeParseError extends Error {
|
|
85
|
+
readonly raw: unknown;
|
|
86
|
+
constructor(message: string, raw: unknown);
|
|
87
|
+
}
|
|
88
|
+
export declare function parseZoneEnvelope(raw: unknown): ZoneEvent;
|
|
89
|
+
export declare function parseZoneEnvelopeJson(json: string): ZoneEvent | null;
|
|
90
|
+
//# sourceMappingURL=zone-event-envelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zone-event-envelope.d.ts","sourceRoot":"","sources":["../../../src/director/zone/zone-event-envelope.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Zone-event envelope + typed payloads (Phase 16, LOOM-DIRECTOR-PROTOCOL-V2 §3).
|
|
2
|
+
//
|
|
3
|
+
// v2 introduces a parallel event surface scoped per zone instead of per
|
|
4
|
+
// character. The Director emits a zone event, the server fans it out to
|
|
5
|
+
// every peer currently in that zone. v1's character-scoped EventEnvelope
|
|
6
|
+
// in ../event-envelope.ts remains the source of truth for v1 events and
|
|
7
|
+
// is NOT touched by this file.
|
|
8
|
+
//
|
|
9
|
+
// Spec invariants preserved:
|
|
10
|
+
// - id is monotonic per zone (NOT global). Each zone is its own
|
|
11
|
+
// append-only log starting at 1.
|
|
12
|
+
// - emitter_id is the character_id that triggered the event, or null
|
|
13
|
+
// when the Loom emitted spontaneously (timer, autonomous Director).
|
|
14
|
+
// - encounter_id is GONE - zone events are zone-scoped, not encounter-
|
|
15
|
+
// scoped.
|
|
16
|
+
// - priority field carries v1 §7.2 semantics: P0 always delivered,
|
|
17
|
+
// P1 dropped first under load, P2 next.
|
|
18
|
+
// - Reuses v1's DropSpec, NarratorVoice, KnotPaletteHex, KnotMood so
|
|
19
|
+
// consumer code that already understands v1 needs zero changes for
|
|
20
|
+
// the overlapping shapes.
|
|
21
|
+
// ----- Priority classes (spec §3.1) -----
|
|
22
|
+
const ZONE_PRIORITY_BY_TYPE = {
|
|
23
|
+
'zone.boss.spawn': 'P0',
|
|
24
|
+
'zone.boss.end': 'P0',
|
|
25
|
+
'zone.snapshot': 'P0',
|
|
26
|
+
'zone.state': 'P0',
|
|
27
|
+
'zone.narrator': 'P1',
|
|
28
|
+
'zone.knot': 'P1',
|
|
29
|
+
'zone.boss.tick': 'P2',
|
|
30
|
+
};
|
|
31
|
+
export function priorityFor(type) {
|
|
32
|
+
return ZONE_PRIORITY_BY_TYPE[type];
|
|
33
|
+
}
|
|
34
|
+
// ----- Parsers -----
|
|
35
|
+
export class ZoneEventEnvelopeParseError extends Error {
|
|
36
|
+
raw;
|
|
37
|
+
constructor(message, raw) {
|
|
38
|
+
super('ZoneEventEnvelopeParseError: ' + message);
|
|
39
|
+
this.raw = raw;
|
|
40
|
+
this.name = 'ZoneEventEnvelopeParseError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const KNOWN_ZONE_EVENT_TYPES = new Set([
|
|
44
|
+
'zone.boss.spawn',
|
|
45
|
+
'zone.boss.tick',
|
|
46
|
+
'zone.boss.end',
|
|
47
|
+
'zone.narrator',
|
|
48
|
+
'zone.knot',
|
|
49
|
+
'zone.state',
|
|
50
|
+
'zone.snapshot',
|
|
51
|
+
]);
|
|
52
|
+
// Validate envelope shape only - data payload validation is the caller's
|
|
53
|
+
// responsibility (downstream type narrowing on `event.type` gives full
|
|
54
|
+
// payload typing). Throws on shape errors so the bridge can decide to
|
|
55
|
+
// drop / log / reconnect. Mirrors parseEnvelope() in v1.
|
|
56
|
+
export function parseZoneEnvelope(raw) {
|
|
57
|
+
if (!raw || typeof raw !== 'object') {
|
|
58
|
+
throw new ZoneEventEnvelopeParseError('envelope is not an object', raw);
|
|
59
|
+
}
|
|
60
|
+
const e = raw;
|
|
61
|
+
if (typeof e['id'] !== 'number' || e['id'] < 0 || !Number.isFinite(e['id'])) {
|
|
62
|
+
throw new ZoneEventEnvelopeParseError('id must be a non-negative number', raw);
|
|
63
|
+
}
|
|
64
|
+
if (typeof e['ts'] !== 'number') {
|
|
65
|
+
throw new ZoneEventEnvelopeParseError('ts must be a number', raw);
|
|
66
|
+
}
|
|
67
|
+
if (typeof e['type'] !== 'string' || !KNOWN_ZONE_EVENT_TYPES.has(e['type'])) {
|
|
68
|
+
throw new ZoneEventEnvelopeParseError('unknown zone event type: ' + String(e['type']), raw);
|
|
69
|
+
}
|
|
70
|
+
if (typeof e['zone_id'] !== 'string' || e['zone_id'].length === 0) {
|
|
71
|
+
throw new ZoneEventEnvelopeParseError('zone_id must be a non-empty string', raw);
|
|
72
|
+
}
|
|
73
|
+
if (e['emitter_id'] !== null && typeof e['emitter_id'] !== 'string') {
|
|
74
|
+
throw new ZoneEventEnvelopeParseError('emitter_id must be string or null', raw);
|
|
75
|
+
}
|
|
76
|
+
if (e['priority'] !== undefined) {
|
|
77
|
+
if (e['priority'] !== 'P0' && e['priority'] !== 'P1' && e['priority'] !== 'P2') {
|
|
78
|
+
throw new ZoneEventEnvelopeParseError('priority must be P0 / P1 / P2 when present, got: ' + String(e['priority']), raw);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!e['data'] || typeof e['data'] !== 'object') {
|
|
82
|
+
throw new ZoneEventEnvelopeParseError('data must be an object', raw);
|
|
83
|
+
}
|
|
84
|
+
return e;
|
|
85
|
+
}
|
|
86
|
+
// Convenience: parse a string (typically an SSE 'data:' line) into an
|
|
87
|
+
// envelope. Returns null on JSON parse error or shape error so the
|
|
88
|
+
// caller can decide whether to log or re-throw.
|
|
89
|
+
export function parseZoneEnvelopeJson(json) {
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(json);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
return parseZoneEnvelope(parsed);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=zone-event-envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zone-event-envelope.js","sourceRoot":"","sources":["../../../src/director/zone/zone-event-envelope.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ZoneEvent } from './zone-event-envelope.js';
|
|
2
|
+
export interface ZoneEventLogEntry {
|
|
3
|
+
recent: ZoneEvent[];
|
|
4
|
+
activeBossId: string | null;
|
|
5
|
+
lastNarratorLine: string | null;
|
|
6
|
+
lastNarratorTtlMs: number;
|
|
7
|
+
eventsApplied: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ZoneEventLog {
|
|
10
|
+
byZone: Map<string, ZoneEventLogEntry>;
|
|
11
|
+
}
|
|
12
|
+
export declare const RESOURCE_ZONE_EVENT_LOG = "zone_event_log";
|
|
13
|
+
export declare const ZONE_RING_SIZE = 32;
|
|
14
|
+
export declare function createZoneEventLog(): ZoneEventLog;
|
|
15
|
+
export declare function getOrCreateZoneEntry(log: ZoneEventLog, zoneId: string): ZoneEventLogEntry;
|
|
16
|
+
export declare function pushZoneEvent(entry: ZoneEventLogEntry, ev: ZoneEvent): void;
|
|
17
|
+
//# sourceMappingURL=zone-event-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zone-event-log.d.ts","sourceRoot":"","sources":["../../../src/director/zone/zone-event-log.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,MAAM,WAAW,iBAAiB;IAEhC,MAAM,EAAE,SAAS,EAAE,CAAC;IAGpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAG5B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAG1B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CACxC;AAED,eAAO,MAAM,uBAAuB,mBAAmB,CAAC;AAExD,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,wBAAgB,kBAAkB,IAAI,YAAY,CAEjD;AAKD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE,MAAM,GACb,iBAAiB,CAYnB;AAKD,wBAAgB,aAAa,CAAC,KAAK,EAAE,iBAAiB,EAAE,EAAE,EAAE,SAAS,GAAG,IAAI,CAM3E"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ZoneEventLog - per-zone ring buffer parallel to v1's
|
|
2
|
+
// DirectorEventLog (LOOM-DIRECTOR-PROTOCOL-V2 §4.4).
|
|
3
|
+
//
|
|
4
|
+
// Renderer reads from this for UI surfaces (boss HP bar, recent loot
|
|
5
|
+
// toast, narrator banner). Per-zone slots so a player who briefly
|
|
6
|
+
// stepped through another zone keeps a separate diagnostic trail per
|
|
7
|
+
// zone they have observed.
|
|
8
|
+
//
|
|
9
|
+
// The map is dense by-zone; a zone's slot is created lazily on the
|
|
10
|
+
// first event for that zone (avoids preallocating slots for zones the
|
|
11
|
+
// player will never enter). The ring buffer per zone caps at
|
|
12
|
+
// ZONE_RING_SIZE = 32 entries, newest first - matches v1's
|
|
13
|
+
// DirectorEventLog.recent semantics.
|
|
14
|
+
export const RESOURCE_ZONE_EVENT_LOG = 'zone_event_log';
|
|
15
|
+
export const ZONE_RING_SIZE = 32;
|
|
16
|
+
export function createZoneEventLog() {
|
|
17
|
+
return { byZone: new Map() };
|
|
18
|
+
}
|
|
19
|
+
// Get or lazily create the slot for a zone. Used by ZoneEventSystem
|
|
20
|
+
// when the first event for a zone arrives. Exported for tests +
|
|
21
|
+
// bench.
|
|
22
|
+
export function getOrCreateZoneEntry(log, zoneId) {
|
|
23
|
+
const existing = log.byZone.get(zoneId);
|
|
24
|
+
if (existing)
|
|
25
|
+
return existing;
|
|
26
|
+
const fresh = {
|
|
27
|
+
recent: [],
|
|
28
|
+
activeBossId: null,
|
|
29
|
+
lastNarratorLine: null,
|
|
30
|
+
lastNarratorTtlMs: 0,
|
|
31
|
+
eventsApplied: 0,
|
|
32
|
+
};
|
|
33
|
+
log.byZone.set(zoneId, fresh);
|
|
34
|
+
return fresh;
|
|
35
|
+
}
|
|
36
|
+
// Push an event into the per-zone ring buffer (newest first) and
|
|
37
|
+
// bump eventsApplied. Pure; does not touch activeBossId / narrator
|
|
38
|
+
// state - those are the system's job to set per event type.
|
|
39
|
+
export function pushZoneEvent(entry, ev) {
|
|
40
|
+
entry.recent.unshift(ev);
|
|
41
|
+
if (entry.recent.length > ZONE_RING_SIZE) {
|
|
42
|
+
entry.recent.length = ZONE_RING_SIZE;
|
|
43
|
+
}
|
|
44
|
+
entry.eventsApplied++;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=zone-event-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zone-event-log.js","sourceRoot":"","sources":["../../../src/director/zone/zone-event-log.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,qDAAqD;AACrD,EAAE;AACF,qEAAqE;AACrE,kEAAkE;AAClE,qEAAqE;AACrE,2BAA2B;AAC3B,EAAE;AACF,mEAAmE;AACnE,sEAAsE;AACtE,6DAA6D;AAC7D,2DAA2D;AAC3D,qCAAqC;AAuBrC,MAAM,CAAC,MAAM,uBAAuB,GAAG,gBAAgB,CAAC;AAExD,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,MAAM,UAAU,kBAAkB;IAChC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;AAC/B,CAAC;AAED,oEAAoE;AACpE,gEAAgE;AAChE,SAAS;AACT,MAAM,UAAU,oBAAoB,CAClC,GAAiB,EACjB,MAAc;IAEd,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,MAAM,KAAK,GAAsB;QAC/B,MAAM,EAAE,EAAE;QACV,YAAY,EAAE,IAAI;QAClB,gBAAgB,EAAE,IAAI;QACtB,iBAAiB,EAAE,CAAC;QACpB,aAAa,EAAE,CAAC;KACjB,CAAC;IACF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9B,OAAO,KAAK,CAAC;AACf,CAAC;AAED,iEAAiE;AACjE,mEAAmE;AACnE,4DAA4D;AAC5D,MAAM,UAAU,aAAa,CAAC,KAAwB,EAAE,EAAa;IACnE,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACzB,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;QACzC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,cAAc,CAAC;IACvC,CAAC;IACD,KAAK,CAAC,aAAa,EAAE,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { System } from '../../system.js';
|
|
2
|
+
import type { World } from '../../world.js';
|
|
3
|
+
export interface ZoneEventSystemOptions {
|
|
4
|
+
currentZone?: () => string | null;
|
|
5
|
+
applyKnotToSharedContext?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare class ZoneEventSystem implements System {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
private readonly currentZone;
|
|
10
|
+
private readonly applyKnotToSharedContext;
|
|
11
|
+
constructor(opts?: ZoneEventSystemOptions);
|
|
12
|
+
update(world: World, _dt: number): void;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=zone-event-system.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zone-event-system.d.ts","sourceRoot":"","sources":["../../../src/director/zone/zone-event-system.ts"],"names":[],"mappings":"AAuCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AA6B5C,MAAM,WAAW,sBAAsB;IAKrC,WAAW,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAIlC,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,qBAAa,eAAgB,YAAW,MAAM;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAiB;IAEtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAU;gBAEvC,IAAI,GAAE,sBAA2B;IAK7C,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;CAkCxC"}
|