@sadhaka/loom-engine 0.12.0 → 0.13.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/README.md +113 -1
- package/dist/components/peer-sprite.d.ts +25 -0
- package/dist/components/peer-sprite.d.ts.map +1 -0
- package/dist/components/peer-sprite.js +48 -0
- package/dist/components/peer-sprite.js.map +1 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/network/mock-multiplayer-bridge.d.ts +34 -0
- package/dist/network/mock-multiplayer-bridge.d.ts.map +1 -0
- package/dist/network/mock-multiplayer-bridge.js +91 -0
- package/dist/network/mock-multiplayer-bridge.js.map +1 -0
- package/dist/network/multiplayer-bridge.d.ts +45 -0
- package/dist/network/multiplayer-bridge.d.ts.map +1 -0
- package/dist/network/multiplayer-bridge.js +34 -0
- package/dist/network/multiplayer-bridge.js.map +1 -0
- package/dist/network/peer-pool.d.ts +46 -0
- package/dist/network/peer-pool.d.ts.map +1 -0
- package/dist/network/peer-pool.js +185 -0
- package/dist/network/peer-pool.js.map +1 -0
- package/dist/network/sse-multiplayer-bridge.d.ts +36 -0
- package/dist/network/sse-multiplayer-bridge.d.ts.map +1 -0
- package/dist/network/sse-multiplayer-bridge.js +264 -0
- package/dist/network/sse-multiplayer-bridge.js.map +1 -0
- package/dist/systems/peer-presence-system.d.ts +19 -0
- package/dist/systems/peer-presence-system.d.ts.map +1 -0
- package/dist/systems/peer-presence-system.js +118 -0
- package/dist/systems/peer-presence-system.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// PeerPool - tracks all known remote peers and their interpolated
|
|
2
|
+
// world position.
|
|
3
|
+
//
|
|
4
|
+
// Each peer keeps the last two known positions (prev, current) with
|
|
5
|
+
// timestamps. At frame time the system asks the pool for the
|
|
6
|
+
// interpolated (x, y) per peer, which is computed as
|
|
7
|
+
// factor = clamp01((nowMs - prevTsMs) / (currentTsMs - prevTsMs))
|
|
8
|
+
// x = lerp(prevX, currentX, factor)
|
|
9
|
+
// y = lerp(prevY, currentY, factor)
|
|
10
|
+
//
|
|
11
|
+
// When a new presence.update arrives, prev <- current, current <- new.
|
|
12
|
+
// The factor saturates at 1 once nowMs passes currentTsMs, so a peer
|
|
13
|
+
// who stops sending updates simply freezes at their last known
|
|
14
|
+
// position rather than extrapolating off into the distance.
|
|
15
|
+
//
|
|
16
|
+
// "Acceptable lag" per the phase 15.1 spec is ~150ms (one update
|
|
17
|
+
// interval at the 10Hz wire rate), which is imperceptible at
|
|
18
|
+
// walk-speed. No CRDT, no client-side prediction beyond the
|
|
19
|
+
// straight-line lerp - those are deferred until shared state extends
|
|
20
|
+
// past raw position.
|
|
21
|
+
//
|
|
22
|
+
// Self-filter: the local character's own character_id should NOT
|
|
23
|
+
// appear among the rendered peers (we don't render ourselves as a
|
|
24
|
+
// ghost). The PeerPresenceSystem owns this filter via
|
|
25
|
+
// setLocalCharacterId(); peers with that id are silently skipped on
|
|
26
|
+
// upsert and removed if already present.
|
|
27
|
+
function lerp(a, b, t) {
|
|
28
|
+
return a + (b - a) * t;
|
|
29
|
+
}
|
|
30
|
+
function clamp01(t) {
|
|
31
|
+
if (t < 0)
|
|
32
|
+
return 0;
|
|
33
|
+
if (t > 1)
|
|
34
|
+
return 1;
|
|
35
|
+
return t;
|
|
36
|
+
}
|
|
37
|
+
export class PeerPool {
|
|
38
|
+
peers = new Map();
|
|
39
|
+
localCharacterId = null;
|
|
40
|
+
// Reused on every getRenderedPosition / forEachRendered call so the
|
|
41
|
+
// hot per-frame path is allocation-free.
|
|
42
|
+
scratchView = {
|
|
43
|
+
characterId: '',
|
|
44
|
+
x: 0,
|
|
45
|
+
y: 0,
|
|
46
|
+
zone: '',
|
|
47
|
+
name: null,
|
|
48
|
+
};
|
|
49
|
+
setLocalCharacterId(id) {
|
|
50
|
+
this.localCharacterId = id;
|
|
51
|
+
if (id !== null && this.peers.has(id)) {
|
|
52
|
+
this.peers.delete(id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
getLocalCharacterId() {
|
|
56
|
+
return this.localCharacterId;
|
|
57
|
+
}
|
|
58
|
+
// Apply a new presence update for a peer. If the peer is the local
|
|
59
|
+
// character, the update is ignored (self-filter). If this is the
|
|
60
|
+
// first update for the peer, prev = current = the new position so
|
|
61
|
+
// the lerp factor immediately saturates and the peer renders at the
|
|
62
|
+
// sent position.
|
|
63
|
+
upsert(characterId, x, y, zone, tsMs, name) {
|
|
64
|
+
if (this.localCharacterId !== null && characterId === this.localCharacterId) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const existing = this.peers.get(characterId);
|
|
68
|
+
if (!existing) {
|
|
69
|
+
this.peers.set(characterId, {
|
|
70
|
+
characterId,
|
|
71
|
+
zone,
|
|
72
|
+
name: name ?? null,
|
|
73
|
+
prevX: x,
|
|
74
|
+
prevY: y,
|
|
75
|
+
prevTsMs: tsMs,
|
|
76
|
+
currentX: x,
|
|
77
|
+
currentY: y,
|
|
78
|
+
currentTsMs: tsMs,
|
|
79
|
+
lastRenderedFrame: -1,
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Out-of-order: drop messages older than current. Wire protocol
|
|
84
|
+
// is monotonic per character_id, but reorder buffers + reconnect
|
|
85
|
+
// replays can deliver an older ts after a newer one.
|
|
86
|
+
if (tsMs < existing.currentTsMs) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
existing.prevX = existing.currentX;
|
|
90
|
+
existing.prevY = existing.currentY;
|
|
91
|
+
existing.prevTsMs = existing.currentTsMs;
|
|
92
|
+
existing.currentX = x;
|
|
93
|
+
existing.currentY = y;
|
|
94
|
+
existing.currentTsMs = tsMs;
|
|
95
|
+
existing.zone = zone;
|
|
96
|
+
if (name !== undefined) {
|
|
97
|
+
existing.name = name;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Replace the entire roster with a snapshot. Peers not present in
|
|
101
|
+
// the snapshot are dropped; peers in the snapshot but not yet
|
|
102
|
+
// tracked are inserted (with prev = current so they render at the
|
|
103
|
+
// sent position immediately).
|
|
104
|
+
applySnapshot(peers) {
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
for (let i = 0; i < peers.length; i++) {
|
|
107
|
+
const p = peers[i];
|
|
108
|
+
if (!p)
|
|
109
|
+
continue;
|
|
110
|
+
if (this.localCharacterId !== null && p.characterId === this.localCharacterId) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
seen.add(p.characterId);
|
|
114
|
+
this.upsert(p.characterId, p.x, p.y, p.zone, p.tsMs, p.name);
|
|
115
|
+
}
|
|
116
|
+
// Drop anyone not in the snapshot. Iterate keys snapshot first
|
|
117
|
+
// because Map.delete during iteration is fine but copying makes
|
|
118
|
+
// intent obvious.
|
|
119
|
+
const toRemove = [];
|
|
120
|
+
this.peers.forEach((_v, k) => {
|
|
121
|
+
if (!seen.has(k))
|
|
122
|
+
toRemove.push(k);
|
|
123
|
+
});
|
|
124
|
+
for (let i = 0; i < toRemove.length; i++) {
|
|
125
|
+
const k = toRemove[i];
|
|
126
|
+
if (k)
|
|
127
|
+
this.peers.delete(k);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
remove(characterId) {
|
|
131
|
+
return this.peers.delete(characterId);
|
|
132
|
+
}
|
|
133
|
+
has(characterId) {
|
|
134
|
+
return this.peers.has(characterId);
|
|
135
|
+
}
|
|
136
|
+
size() {
|
|
137
|
+
return this.peers.size;
|
|
138
|
+
}
|
|
139
|
+
get(characterId) {
|
|
140
|
+
return this.peers.get(characterId);
|
|
141
|
+
}
|
|
142
|
+
// Iterate every tracked peer with their interpolated world
|
|
143
|
+
// position at nowMs. The view object is reused; consumers must
|
|
144
|
+
// copy any field they want to retain past the callback.
|
|
145
|
+
forEachRendered(nowMs, frame, fn) {
|
|
146
|
+
this.peers.forEach((entry) => {
|
|
147
|
+
const v = this.scratchView;
|
|
148
|
+
v.characterId = entry.characterId;
|
|
149
|
+
v.zone = entry.zone;
|
|
150
|
+
v.name = entry.name;
|
|
151
|
+
const dt = entry.currentTsMs - entry.prevTsMs;
|
|
152
|
+
if (dt <= 0) {
|
|
153
|
+
v.x = entry.currentX;
|
|
154
|
+
v.y = entry.currentY;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const t = clamp01((nowMs - entry.prevTsMs) / dt);
|
|
158
|
+
v.x = lerp(entry.prevX, entry.currentX, t);
|
|
159
|
+
v.y = lerp(entry.prevY, entry.currentY, t);
|
|
160
|
+
}
|
|
161
|
+
entry.lastRenderedFrame = frame;
|
|
162
|
+
fn(v);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// Single-peer query for tests + the rare ad-hoc lookup. Hot paths
|
|
166
|
+
// use forEachRendered to avoid map lookups.
|
|
167
|
+
getRenderedPosition(characterId, nowMs) {
|
|
168
|
+
const entry = this.peers.get(characterId);
|
|
169
|
+
if (!entry)
|
|
170
|
+
return null;
|
|
171
|
+
const dt = entry.currentTsMs - entry.prevTsMs;
|
|
172
|
+
if (dt <= 0) {
|
|
173
|
+
return { x: entry.currentX, y: entry.currentY };
|
|
174
|
+
}
|
|
175
|
+
const t = clamp01((nowMs - entry.prevTsMs) / dt);
|
|
176
|
+
return {
|
|
177
|
+
x: lerp(entry.prevX, entry.currentX, t),
|
|
178
|
+
y: lerp(entry.prevY, entry.currentY, t),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
clear() {
|
|
182
|
+
this.peers.clear();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=peer-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer-pool.js","sourceRoot":"","sources":["../../src/network/peer-pool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type IMultiplayerBridge, type MultiplayerBridgeStatus, type MultiplayerBridgeStats, type PresenceMessage } from './multiplayer-bridge.js';
|
|
2
|
+
export interface SSEMultiplayerBridgeOptions {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
broadcastUrl?: string;
|
|
5
|
+
characterId: string;
|
|
6
|
+
zone: string;
|
|
7
|
+
eventSourceFactory?: (url: string) => EventSource;
|
|
8
|
+
fetchFn?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
export declare class SSEMultiplayerBridge implements IMultiplayerBridge {
|
|
11
|
+
private readonly baseUrl;
|
|
12
|
+
private readonly broadcastUrl;
|
|
13
|
+
private readonly characterId;
|
|
14
|
+
private readonly zone;
|
|
15
|
+
private readonly eventSourceFactory;
|
|
16
|
+
private readonly fetchFn;
|
|
17
|
+
private es;
|
|
18
|
+
private queue;
|
|
19
|
+
private statusValue;
|
|
20
|
+
private statsValue;
|
|
21
|
+
private lastBroadcastMs;
|
|
22
|
+
constructor(opts: SSEMultiplayerBridgeOptions);
|
|
23
|
+
connect(): void;
|
|
24
|
+
disconnect(): void;
|
|
25
|
+
status(): MultiplayerBridgeStatus;
|
|
26
|
+
pollMessages(): PresenceMessage[];
|
|
27
|
+
broadcastPosition(x: number, y: number, zone: string, tsMs: number): void;
|
|
28
|
+
stats(): Readonly<MultiplayerBridgeStats>;
|
|
29
|
+
private buildUrl;
|
|
30
|
+
private openConnection;
|
|
31
|
+
private closeConnection;
|
|
32
|
+
private handleUpdate;
|
|
33
|
+
private handleDepart;
|
|
34
|
+
private handleSnapshot;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=sse-multiplayer-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-multiplayer-bridge.d.ts","sourceRoot":"","sources":["../../src/network/sse-multiplayer-bridge.ts"],"names":[],"mappings":"AA4BA,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EAErB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,2BAA2B;IAI1C,OAAO,EAAE,MAAM,CAAC;IAKhB,YAAY,CAAC,EAAE,MAAM,CAAC;IAItB,WAAW,EAAE,MAAM,CAAC;IAEpB,IAAI,EAAE,MAAM,CAAC;IAGb,kBAAkB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,WAAW,CAAC;IAClD,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED,qBAAa,oBAAqB,YAAW,kBAAkB;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA+B;IAClE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC,OAAO,CAAC,EAAE,CAA4B;IACtC,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,UAAU,CAKhB;IAEF,OAAO,CAAC,eAAe,CAAqB;gBAEhC,IAAI,EAAE,2BAA2B;IAwB7C,OAAO,IAAI,IAAI;IAMf,UAAU,IAAI,IAAI;IAKlB,MAAM,IAAI,uBAAuB;IAIjC,YAAY,IAAI,eAAe,EAAE;IAOjC,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IA0BzE,KAAK,IAAI,QAAQ,CAAC,sBAAsB,CAAC;IAMzC,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,cAAc;IAmCtB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,YAAY;IAyBpB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,cAAc;CAsCvB"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// SSEMultiplayerBridge - real EventSource subscription to the
|
|
2
|
+
// backend's presence endpoint, paired with a fetch POST for outbound
|
|
3
|
+
// position broadcasts.
|
|
4
|
+
//
|
|
5
|
+
// Wire protocol (paired with Track B server-side):
|
|
6
|
+
// GET <baseUrl>?character_id=...&zone=... opens an SSE stream that
|
|
7
|
+
// emits three event types:
|
|
8
|
+
// - 'presence.snapshot' { peers: [{ character_id, x, y, zone, ts_ms, name? }] }
|
|
9
|
+
// emitted once on connect with the full current peer roster.
|
|
10
|
+
// - 'presence.update' { character_id, x, y, zone, ts_ms, name? }
|
|
11
|
+
// emitted as peers move.
|
|
12
|
+
// - 'presence.depart' { character_id }
|
|
13
|
+
// emitted when a peer disconnects.
|
|
14
|
+
//
|
|
15
|
+
// POST <broadcastUrl> { character_id, x, y, zone, ts_ms }
|
|
16
|
+
// called by broadcastPosition at most BROADCAST_HZ per second.
|
|
17
|
+
// Engine-side rate limit; the bridge silently drops excess calls
|
|
18
|
+
// and increments rateLimitedDrops.
|
|
19
|
+
//
|
|
20
|
+
// Reconnect strategy: EventSource handles transport-layer reconnect
|
|
21
|
+
// internally. We observe via onerror/onopen and surface 'reconnecting'
|
|
22
|
+
// to the consumer. On reconnect the server is expected to re-emit a
|
|
23
|
+
// fresh 'presence.snapshot', which the PeerPool consumes and treats
|
|
24
|
+
// as authoritative (any peer not in the snapshot is dropped).
|
|
25
|
+
//
|
|
26
|
+
// Browser-only. Constructor throws if EventSource is undefined (Node
|
|
27
|
+
// test environment). Tests use MockMultiplayerBridge instead.
|
|
28
|
+
import { BROADCAST_MIN_INTERVAL_MS, } from './multiplayer-bridge.js';
|
|
29
|
+
export class SSEMultiplayerBridge {
|
|
30
|
+
baseUrl;
|
|
31
|
+
broadcastUrl;
|
|
32
|
+
characterId;
|
|
33
|
+
zone;
|
|
34
|
+
eventSourceFactory;
|
|
35
|
+
fetchFn;
|
|
36
|
+
es = null;
|
|
37
|
+
queue = [];
|
|
38
|
+
statusValue = 'idle';
|
|
39
|
+
statsValue = {
|
|
40
|
+
messagesReceived: 0,
|
|
41
|
+
messagesSent: 0,
|
|
42
|
+
rateLimitedDrops: 0,
|
|
43
|
+
reconnects: 0,
|
|
44
|
+
};
|
|
45
|
+
lastBroadcastMs = -Infinity;
|
|
46
|
+
constructor(opts) {
|
|
47
|
+
this.baseUrl = opts.baseUrl;
|
|
48
|
+
this.broadcastUrl = opts.broadcastUrl ?? defaultBroadcastUrl(opts.baseUrl);
|
|
49
|
+
this.characterId = opts.characterId;
|
|
50
|
+
this.zone = opts.zone;
|
|
51
|
+
if (opts.eventSourceFactory) {
|
|
52
|
+
this.eventSourceFactory = opts.eventSourceFactory;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
if (typeof EventSource === 'undefined') {
|
|
56
|
+
throw new Error('SSEMultiplayerBridge: EventSource is not available in this environment. Use MockMultiplayerBridge for tests.');
|
|
57
|
+
}
|
|
58
|
+
const ESCtor = EventSource;
|
|
59
|
+
this.eventSourceFactory = (u) => new ESCtor(u, { withCredentials: true });
|
|
60
|
+
}
|
|
61
|
+
if (opts.fetchFn) {
|
|
62
|
+
this.fetchFn = opts.fetchFn;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
if (typeof fetch === 'undefined') {
|
|
66
|
+
throw new Error('SSEMultiplayerBridge: fetch is not available in this environment.');
|
|
67
|
+
}
|
|
68
|
+
this.fetchFn = fetch.bind(globalThis);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
connect() {
|
|
72
|
+
if (this.es)
|
|
73
|
+
return;
|
|
74
|
+
this.statusValue = 'connecting';
|
|
75
|
+
this.openConnection();
|
|
76
|
+
}
|
|
77
|
+
disconnect() {
|
|
78
|
+
this.statusValue = 'closed';
|
|
79
|
+
this.closeConnection();
|
|
80
|
+
}
|
|
81
|
+
status() {
|
|
82
|
+
return this.statusValue;
|
|
83
|
+
}
|
|
84
|
+
pollMessages() {
|
|
85
|
+
if (this.queue.length === 0)
|
|
86
|
+
return [];
|
|
87
|
+
const out = this.queue;
|
|
88
|
+
this.queue = [];
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
broadcastPosition(x, y, zone, tsMs) {
|
|
92
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
93
|
+
if (now - this.lastBroadcastMs < BROADCAST_MIN_INTERVAL_MS) {
|
|
94
|
+
this.statsValue.rateLimitedDrops++;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.lastBroadcastMs = now;
|
|
98
|
+
this.statsValue.messagesSent++;
|
|
99
|
+
// Fire-and-forget POST. Errors are surfaced via stats only - the
|
|
100
|
+
// engine doesn't block on the network round trip. The body
|
|
101
|
+
// matches the server contract from the phase 15.1 spec.
|
|
102
|
+
const body = JSON.stringify({
|
|
103
|
+
character_id: this.characterId,
|
|
104
|
+
x,
|
|
105
|
+
y,
|
|
106
|
+
zone,
|
|
107
|
+
ts_ms: tsMs,
|
|
108
|
+
});
|
|
109
|
+
void this.fetchFn(this.broadcastUrl, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
credentials: 'include',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body,
|
|
114
|
+
}).catch(() => { });
|
|
115
|
+
}
|
|
116
|
+
stats() {
|
|
117
|
+
return this.statsValue;
|
|
118
|
+
}
|
|
119
|
+
// ----- Internal -----
|
|
120
|
+
buildUrl() {
|
|
121
|
+
const sep = this.baseUrl.includes('?') ? '&' : '?';
|
|
122
|
+
return (this.baseUrl +
|
|
123
|
+
sep +
|
|
124
|
+
'character_id=' + encodeURIComponent(this.characterId) +
|
|
125
|
+
'&zone=' + encodeURIComponent(this.zone));
|
|
126
|
+
}
|
|
127
|
+
openConnection() {
|
|
128
|
+
const url = this.buildUrl();
|
|
129
|
+
let es;
|
|
130
|
+
try {
|
|
131
|
+
es = this.eventSourceFactory(url);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
this.statusValue = 'closed';
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
this.es = es;
|
|
138
|
+
es.onopen = () => {
|
|
139
|
+
this.statusValue = 'connected';
|
|
140
|
+
};
|
|
141
|
+
es.onerror = () => {
|
|
142
|
+
const closed = es.readyState === 2; // EventSource.CLOSED
|
|
143
|
+
if (closed) {
|
|
144
|
+
this.statusValue = 'closed';
|
|
145
|
+
this.closeConnection();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.statusValue = 'reconnecting';
|
|
149
|
+
this.statsValue.reconnects++;
|
|
150
|
+
};
|
|
151
|
+
es.addEventListener('presence.update', (e) => {
|
|
152
|
+
this.handleUpdate(e);
|
|
153
|
+
});
|
|
154
|
+
es.addEventListener('presence.depart', (e) => {
|
|
155
|
+
this.handleDepart(e);
|
|
156
|
+
});
|
|
157
|
+
es.addEventListener('presence.snapshot', (e) => {
|
|
158
|
+
this.handleSnapshot(e);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
closeConnection() {
|
|
162
|
+
if (!this.es)
|
|
163
|
+
return;
|
|
164
|
+
try {
|
|
165
|
+
this.es.close();
|
|
166
|
+
}
|
|
167
|
+
catch { /* ignore */ }
|
|
168
|
+
this.es = null;
|
|
169
|
+
}
|
|
170
|
+
handleUpdate(e) {
|
|
171
|
+
const data = parseJson(e.data);
|
|
172
|
+
if (!data || typeof data !== 'object')
|
|
173
|
+
return;
|
|
174
|
+
const characterId = data.character_id;
|
|
175
|
+
const x = data.x;
|
|
176
|
+
const y = data.y;
|
|
177
|
+
const zone = data.zone;
|
|
178
|
+
const tsMs = data.ts_ms;
|
|
179
|
+
if (typeof characterId !== 'string')
|
|
180
|
+
return;
|
|
181
|
+
if (typeof x !== 'number' || typeof y !== 'number')
|
|
182
|
+
return;
|
|
183
|
+
if (typeof zone !== 'string')
|
|
184
|
+
return;
|
|
185
|
+
if (typeof tsMs !== 'number')
|
|
186
|
+
return;
|
|
187
|
+
const name = data.name;
|
|
188
|
+
this.statsValue.messagesReceived++;
|
|
189
|
+
this.queue.push({
|
|
190
|
+
kind: 'update',
|
|
191
|
+
characterId,
|
|
192
|
+
x,
|
|
193
|
+
y,
|
|
194
|
+
zone,
|
|
195
|
+
tsMs,
|
|
196
|
+
...(typeof name === 'string' ? { name } : {}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
handleDepart(e) {
|
|
200
|
+
const data = parseJson(e.data);
|
|
201
|
+
if (!data || typeof data !== 'object')
|
|
202
|
+
return;
|
|
203
|
+
const characterId = data.character_id;
|
|
204
|
+
if (typeof characterId !== 'string')
|
|
205
|
+
return;
|
|
206
|
+
this.statsValue.messagesReceived++;
|
|
207
|
+
this.queue.push({ kind: 'depart', characterId });
|
|
208
|
+
}
|
|
209
|
+
handleSnapshot(e) {
|
|
210
|
+
const data = parseJson(e.data);
|
|
211
|
+
if (!data || typeof data !== 'object')
|
|
212
|
+
return;
|
|
213
|
+
const peersRaw = data.peers;
|
|
214
|
+
if (!Array.isArray(peersRaw))
|
|
215
|
+
return;
|
|
216
|
+
const peers = [];
|
|
217
|
+
for (let i = 0; i < peersRaw.length; i++) {
|
|
218
|
+
const p = peersRaw[i];
|
|
219
|
+
if (!p || typeof p !== 'object')
|
|
220
|
+
continue;
|
|
221
|
+
const characterId = p.character_id;
|
|
222
|
+
const x = p.x;
|
|
223
|
+
const y = p.y;
|
|
224
|
+
const zone = p.zone;
|
|
225
|
+
const tsMs = p.ts_ms;
|
|
226
|
+
if (typeof characterId !== 'string')
|
|
227
|
+
continue;
|
|
228
|
+
if (typeof x !== 'number' || typeof y !== 'number')
|
|
229
|
+
continue;
|
|
230
|
+
if (typeof zone !== 'string')
|
|
231
|
+
continue;
|
|
232
|
+
if (typeof tsMs !== 'number')
|
|
233
|
+
continue;
|
|
234
|
+
const name = p.name;
|
|
235
|
+
peers.push({
|
|
236
|
+
characterId,
|
|
237
|
+
x,
|
|
238
|
+
y,
|
|
239
|
+
zone,
|
|
240
|
+
tsMs,
|
|
241
|
+
...(typeof name === 'string' ? { name } : {}),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
this.statsValue.messagesReceived++;
|
|
245
|
+
this.queue.push({ kind: 'snapshot', peers });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function parseJson(raw) {
|
|
249
|
+
if (typeof raw !== 'string')
|
|
250
|
+
return null;
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(raw);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function defaultBroadcastUrl(baseUrl) {
|
|
259
|
+
if (baseUrl.endsWith('/events')) {
|
|
260
|
+
return baseUrl.slice(0, -'/events'.length) + '/move';
|
|
261
|
+
}
|
|
262
|
+
return baseUrl + '/move';
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=sse-multiplayer-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-multiplayer-bridge.js","sourceRoot":"","sources":["../../src/network/sse-multiplayer-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { System } from '../system.js';
|
|
2
|
+
import type { World } from '../world.js';
|
|
3
|
+
export declare class PeerPresenceSystem implements System {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
update(world: World, _dt: number): void;
|
|
6
|
+
}
|
|
7
|
+
export declare class PeerRenderSystem implements System {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
private scratchTextStyle;
|
|
10
|
+
private scratchTint;
|
|
11
|
+
private readonly labelYOffset;
|
|
12
|
+
private readonly showNames;
|
|
13
|
+
constructor(opts?: {
|
|
14
|
+
labelYOffset?: number;
|
|
15
|
+
showNames?: boolean;
|
|
16
|
+
});
|
|
17
|
+
update(world: World, _dt: number): void;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=peer-presence-system.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer-presence-system.d.ts","sourceRoot":"","sources":["../../src/systems/peer-presence-system.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAiBzC,qBAAa,kBAAmB,YAAW,MAAM;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAmB;IAExC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;CAwBxC;AAKD,qBAAa,gBAAiB,YAAW,MAAM;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAiB;IAGtC,OAAO,CAAC,gBAAgB,CAKtB;IACF,OAAO,CAAC,WAAW,CAEjB;IAMF,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;gBAExB,IAAI,GAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO;IAKrE,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;CAgCxC"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// PeerPresenceSystem - drains the IMultiplayerBridge each tick, applies
|
|
2
|
+
// presence messages to the PeerPool, and (optionally) broadcasts the
|
|
3
|
+
// local character's position via PeerBroadcastBridge.
|
|
4
|
+
//
|
|
5
|
+
// PeerRenderSystem - iterates PeerPool at frame time, computes each
|
|
6
|
+
// peer's interpolated position from prev/current samples, looks up
|
|
7
|
+
// their sprite via PeerSpritePool, and submits drawSprite + drawText
|
|
8
|
+
// (name label) calls to the device.
|
|
9
|
+
//
|
|
10
|
+
// Phasing rationale (matches DirectorSystem):
|
|
11
|
+
// PHASE_INPUT - PeerPresenceSystem drains the bridge so peers are
|
|
12
|
+
// fresh before any logic system reads them. Mirrors
|
|
13
|
+
// the DirectorSystem ordering.
|
|
14
|
+
// PHASE_RENDER - PeerRenderSystem submits draw calls. Per-peer
|
|
15
|
+
// interp factor is computed from the current
|
|
16
|
+
// TimeResource clock, so peers move smoothly even on
|
|
17
|
+
// frames with no inbound update.
|
|
18
|
+
//
|
|
19
|
+
// PeerPresenceSystem also routes the snapshot, update, and depart
|
|
20
|
+
// message kinds to the right PeerPool method:
|
|
21
|
+
// snapshot -> applySnapshot (drops anyone not in the snapshot)
|
|
22
|
+
// update -> upsert
|
|
23
|
+
// depart -> remove
|
|
24
|
+
//
|
|
25
|
+
// Self-filter: callers register the local character_id via
|
|
26
|
+
// peerPool.setLocalCharacterId(...) at startup. The pool refuses to
|
|
27
|
+
// store an entry with that id; if a snapshot or update mentions it,
|
|
28
|
+
// it's silently skipped (we don't render ourselves as a ghost).
|
|
29
|
+
import { RESOURCE_MULTIPLAYER_BRIDGE, RESOURCE_PEER_POOL, } from '../network/multiplayer-bridge.js';
|
|
30
|
+
import { POOL_PEER_SPRITE } from '../components/peer-sprite.js';
|
|
31
|
+
import { RESOURCE_DEVICE, RESOURCE_CAMERA, RESOURCE_TIME, } from '../resources.js';
|
|
32
|
+
export class PeerPresenceSystem {
|
|
33
|
+
name = 'peer-presence';
|
|
34
|
+
update(world, _dt) {
|
|
35
|
+
const bridge = world.resources.get(RESOURCE_MULTIPLAYER_BRIDGE);
|
|
36
|
+
const pool = world.resources.get(RESOURCE_PEER_POOL);
|
|
37
|
+
if (!bridge || !pool)
|
|
38
|
+
return;
|
|
39
|
+
const messages = bridge.pollMessages();
|
|
40
|
+
if (messages.length === 0)
|
|
41
|
+
return;
|
|
42
|
+
for (let i = 0; i < messages.length; i++) {
|
|
43
|
+
const m = messages[i];
|
|
44
|
+
if (!m)
|
|
45
|
+
continue;
|
|
46
|
+
switch (m.kind) {
|
|
47
|
+
case 'update':
|
|
48
|
+
pool.upsert(m.characterId, m.x, m.y, m.zone, m.tsMs, m.name);
|
|
49
|
+
break;
|
|
50
|
+
case 'depart':
|
|
51
|
+
pool.remove(m.characterId);
|
|
52
|
+
break;
|
|
53
|
+
case 'snapshot':
|
|
54
|
+
pool.applySnapshot(m.peers);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Render-phase counterpart. Optional - consumers who only want to
|
|
61
|
+
// surface peer state (e.g. on a minimap) can read PeerPool directly
|
|
62
|
+
// and skip this system.
|
|
63
|
+
export class PeerRenderSystem {
|
|
64
|
+
name = 'peer-render';
|
|
65
|
+
// Reused per-frame to avoid allocation in the per-peer hot path.
|
|
66
|
+
scratchTextStyle = {
|
|
67
|
+
font: '12px sans-serif',
|
|
68
|
+
fill: { r: 1, g: 1, b: 1, a: 1 },
|
|
69
|
+
align: 'center',
|
|
70
|
+
baseline: 'bottom',
|
|
71
|
+
};
|
|
72
|
+
scratchTint = {
|
|
73
|
+
r: 1, g: 1, b: 1, a: 1,
|
|
74
|
+
};
|
|
75
|
+
// Vertical offset (world units) applied to the name label so it
|
|
76
|
+
// sits just above the sprite. Tuned for the engine's standard
|
|
77
|
+
// 64-px sprite cell; consumers with custom art can override via
|
|
78
|
+
// the PeerRenderSystem constructor.
|
|
79
|
+
labelYOffset;
|
|
80
|
+
showNames;
|
|
81
|
+
constructor(opts = {}) {
|
|
82
|
+
this.labelYOffset = opts.labelYOffset ?? -32;
|
|
83
|
+
this.showNames = opts.showNames ?? true;
|
|
84
|
+
}
|
|
85
|
+
update(world, _dt) {
|
|
86
|
+
const pool = world.resources.get(RESOURCE_PEER_POOL);
|
|
87
|
+
const sprites = world.getPool(POOL_PEER_SPRITE);
|
|
88
|
+
const device = world.resources.get(RESOURCE_DEVICE);
|
|
89
|
+
const camera = world.resources.get(RESOURCE_CAMERA);
|
|
90
|
+
const time = world.resources.get(RESOURCE_TIME);
|
|
91
|
+
if (!pool || !sprites || !device || !camera)
|
|
92
|
+
return;
|
|
93
|
+
device.setCamera(camera);
|
|
94
|
+
const nowMs = (time ? time.elapsed * 1000 : 0) + (typeof performance !== 'undefined' ? performance.now() : 0);
|
|
95
|
+
const frame = time ? time.frame : -1;
|
|
96
|
+
// Capture the values we need from each peer before drawing.
|
|
97
|
+
// forEachRendered's view is reused; we have to read fields out
|
|
98
|
+
// before the next iteration mutates the scratch.
|
|
99
|
+
pool.forEachRendered(nowMs, frame, (view) => {
|
|
100
|
+
const entry = sprites.resolve(view.characterId);
|
|
101
|
+
if (entry.tint) {
|
|
102
|
+
const t = this.scratchTint;
|
|
103
|
+
t.r = entry.tint.r;
|
|
104
|
+
t.g = entry.tint.g;
|
|
105
|
+
t.b = entry.tint.b;
|
|
106
|
+
t.a = entry.tint.a;
|
|
107
|
+
device.drawSprite(view.x, view.y, 0, entry.atlas, entry.frame, t);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
device.drawSprite(view.x, view.y, 0, entry.atlas, entry.frame);
|
|
111
|
+
}
|
|
112
|
+
if (this.showNames && view.name) {
|
|
113
|
+
device.drawText(view.x, view.y + this.labelYOffset, view.name, this.scratchTextStyle);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=peer-presence-system.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer-presence-system.js","sourceRoot":"","sources":["../../src/systems/peer-presence-system.ts"],"names":[],"mappings":"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"}
|