@unboxy/phaser-sdk 0.2.13 → 0.2.14
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/SDK-GUIDE.md +1 -0
- package/dist/realtime/UnboxyRoom.d.ts +26 -9
- package/dist/realtime/UnboxyRoom.js +81 -42
- package/package.json +1 -1
package/SDK-GUIDE.md
CHANGED
|
@@ -544,6 +544,7 @@ Don't blindly apply interpolation to everything. It's wrong for a chess piece an
|
|
|
544
544
|
|
|
545
545
|
## Changelog
|
|
546
546
|
|
|
547
|
+
- **0.2.14** — fix: `ChatFacade` `system.joined` / `system.left` events were silently no-op'ing in production. The lifecycle subscription used `state.players.onAdd` / `.onRemove` instance methods that Colyseus 0.16 removed in favour of `getStateCallbacks(room)`; the runtime check `typeof players.onAdd === 'function'` evaluated false, so neither system message fired. Now uses `room.onStateChange` + a known-names diff — robust to future Colyseus API changes. Tests updated to match. Patch-only; no API change for game code.
|
|
547
548
|
- **0.2.13** — added `room.chat` helper for in-game chat. New API: `room.chat.send(text)` (returns `Promise<void>`) + `room.chat.onMessage(handler)`, plus exports `ChatMessage`, `ChatMessageKind`, and `MAX_CHAT_TEXT_LEN`. Wraps the existing `'chat'` wildcard relay with local echo (sender sees own message synchronously), stamped `displayName` on every payload, trim + 500-char silent truncate, and auto-emitted `kind: 'system.joined'` / `'system.left'` messages computed locally from `room.state.players` add/remove events (no wire traffic). Replaces the raw `room.send('chat', text)` pattern in the Multiplayer chapter — raw still works but loses local echo, length cap, and join/leave events.
|
|
548
549
|
- **0.2.12** — added `unboxy.rooms.list(options?)` — returns the open rooms for the caller's game right now, scoped server-side to the caller's gameId. Use this to build a lobby browser ("show me open rooms, click to join") without needing an out-of-band room code. SDK-GUIDE adds a "Browsing open rooms" section under Multiplayer covering the `RoomListEntry` shape, the poll-on-timer pattern, and the `{ roomCode }` filter for "is room X up yet?" probes.
|
|
549
550
|
- **0.2.11** — added `roomCode` + `maxClients` to `JoinOptions`. SDK-GUIDE's Multiplayer chapter now documents matchmaking: room type is always `'lobby'`; use `roomCode` to bucket independent rooms per gameId (create-with-fresh-code, join-with-shared-code, quick-match with empty code). Fixes a real bug where agents were passing a gameId-derived string as the room type (e.g. `joinOrCreate('blokus-<code>', ...)`) and failing with "no handler" server-side.
|
|
@@ -83,10 +83,13 @@ export declare class ChatFacade {
|
|
|
83
83
|
private readonly room;
|
|
84
84
|
private readonly handlers;
|
|
85
85
|
private remoteOff;
|
|
86
|
-
/** Sids
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
|
|
86
|
+
/** Sids known to this facade, mapped to the displayName they had at the
|
|
87
|
+
* time we last observed them. Populated at construction-time snapshot
|
|
88
|
+
* (so we don't emit "X joined" for everyone already present when the local
|
|
89
|
+
* user joins) and on every subsequent diff. We track the displayName here
|
|
90
|
+
* because by the time a player is detected as "left" they're already gone
|
|
91
|
+
* from `state.players` and we can't read it from there. */
|
|
92
|
+
private readonly knownNames;
|
|
90
93
|
constructor(room: Room<unknown>);
|
|
91
94
|
/**
|
|
92
95
|
* Send a chat line. Empty-after-trim input is dropped silently. Text longer
|
|
@@ -108,13 +111,27 @@ export declare class ChatFacade {
|
|
|
108
111
|
*/
|
|
109
112
|
onMessage(handler: (msg: ChatMessage) => void): Unsubscribe;
|
|
110
113
|
/**
|
|
111
|
-
* Snapshot the current player list and
|
|
112
|
-
*
|
|
113
|
-
* constructed), Colyseus has delivered the initial
|
|
114
|
-
*
|
|
115
|
-
*
|
|
114
|
+
* Snapshot the current player list and hook `onStateChange` to detect
|
|
115
|
+
* subsequent joins / leaves. By the time `client.joinOrCreate` resolves
|
|
116
|
+
* (and UnboxyRoom is constructed), Colyseus has delivered the initial
|
|
117
|
+
* state — so the snapshot covers everyone already in the room and we skip
|
|
118
|
+
* emission for them.
|
|
119
|
+
*
|
|
120
|
+
* Why `onStateChange` and not `MapSchema.onAdd` / `.onRemove`: Colyseus
|
|
121
|
+
* 0.16 dropped those instance methods in favour of a separate
|
|
122
|
+
* `getStateCallbacks(room)` proxy API. Hooking `onStateChange` and diffing
|
|
123
|
+
* `players` keys ourselves works the same in any Colyseus version, keeps
|
|
124
|
+
* us decoupled from the realtime backend's callback shape, and adds < 1ms
|
|
125
|
+
* of work per state change for typical room sizes (max 16 players). The
|
|
126
|
+
* earlier 0.2.13 implementation tried `players.onAdd` directly — the
|
|
127
|
+
* conditional silently no-op'd in 0.16 and BOTH `system.joined` /
|
|
128
|
+
* `system.left` events stopped firing in production. Surfaced via a Blokus
|
|
129
|
+
* chat game on 2026-04-27.
|
|
116
130
|
*/
|
|
117
131
|
private subscribeToPlayers;
|
|
132
|
+
/** Compare current `players` membership against `knownNames`, emit
|
|
133
|
+
* `system.joined` for new sids and `system.left` for vanished sids. */
|
|
134
|
+
private diffPlayers;
|
|
118
135
|
private dispatch;
|
|
119
136
|
}
|
|
120
137
|
/**
|
|
@@ -76,10 +76,13 @@ export class ChatFacade {
|
|
|
76
76
|
this.room = room;
|
|
77
77
|
this.handlers = new Set();
|
|
78
78
|
this.remoteOff = null;
|
|
79
|
-
/** Sids
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
79
|
+
/** Sids known to this facade, mapped to the displayName they had at the
|
|
80
|
+
* time we last observed them. Populated at construction-time snapshot
|
|
81
|
+
* (so we don't emit "X joined" for everyone already present when the local
|
|
82
|
+
* user joins) and on every subsequent diff. We track the displayName here
|
|
83
|
+
* because by the time a player is detected as "left" they're already gone
|
|
84
|
+
* from `state.players` and we can't read it from there. */
|
|
85
|
+
this.knownNames = new Map();
|
|
83
86
|
this.subscribeToPlayers();
|
|
84
87
|
}
|
|
85
88
|
/**
|
|
@@ -155,11 +158,22 @@ export class ChatFacade {
|
|
|
155
158
|
};
|
|
156
159
|
}
|
|
157
160
|
/**
|
|
158
|
-
* Snapshot the current player list and
|
|
159
|
-
*
|
|
160
|
-
* constructed), Colyseus has delivered the initial
|
|
161
|
-
*
|
|
162
|
-
*
|
|
161
|
+
* Snapshot the current player list and hook `onStateChange` to detect
|
|
162
|
+
* subsequent joins / leaves. By the time `client.joinOrCreate` resolves
|
|
163
|
+
* (and UnboxyRoom is constructed), Colyseus has delivered the initial
|
|
164
|
+
* state — so the snapshot covers everyone already in the room and we skip
|
|
165
|
+
* emission for them.
|
|
166
|
+
*
|
|
167
|
+
* Why `onStateChange` and not `MapSchema.onAdd` / `.onRemove`: Colyseus
|
|
168
|
+
* 0.16 dropped those instance methods in favour of a separate
|
|
169
|
+
* `getStateCallbacks(room)` proxy API. Hooking `onStateChange` and diffing
|
|
170
|
+
* `players` keys ourselves works the same in any Colyseus version, keeps
|
|
171
|
+
* us decoupled from the realtime backend's callback shape, and adds < 1ms
|
|
172
|
+
* of work per state change for typical room sizes (max 16 players). The
|
|
173
|
+
* earlier 0.2.13 implementation tried `players.onAdd` directly — the
|
|
174
|
+
* conditional silently no-op'd in 0.16 and BOTH `system.joined` /
|
|
175
|
+
* `system.left` events stopped firing in production. Surfaced via a Blokus
|
|
176
|
+
* chat game on 2026-04-27.
|
|
163
177
|
*/
|
|
164
178
|
subscribeToPlayers() {
|
|
165
179
|
const players = this.room.state?.players;
|
|
@@ -167,49 +181,74 @@ export class ChatFacade {
|
|
|
167
181
|
return;
|
|
168
182
|
if (typeof players.forEach === 'function') {
|
|
169
183
|
try {
|
|
170
|
-
players.forEach((
|
|
184
|
+
players.forEach((p, sid) => {
|
|
185
|
+
const name = typeof p?.displayName === 'string' ? p.displayName : '';
|
|
186
|
+
this.knownNames.set(sid, name);
|
|
187
|
+
});
|
|
171
188
|
}
|
|
172
189
|
catch {
|
|
173
|
-
// forEach can throw on an unsynced state;
|
|
190
|
+
// forEach can throw on an unsynced state; knownNames stays empty,
|
|
174
191
|
// which means we'll emit "joined" for the initial hydration. That's
|
|
175
192
|
// a degraded but non-broken behavior — the chat log gets an extra
|
|
176
193
|
// line per existing player. Log once for visibility.
|
|
177
194
|
console.warn('[unboxy.chat] could not snapshot players on subscribe');
|
|
178
195
|
}
|
|
179
196
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
// Diff on every state change. Cheap for any reasonable room size
|
|
198
|
+
// (Colyseus caps maxClients at 16 server-side).
|
|
199
|
+
this.room.onStateChange(() => this.diffPlayers());
|
|
200
|
+
}
|
|
201
|
+
/** Compare current `players` membership against `knownNames`, emit
|
|
202
|
+
* `system.joined` for new sids and `system.left` for vanished sids. */
|
|
203
|
+
diffPlayers() {
|
|
204
|
+
const state = this.room.state;
|
|
205
|
+
const players = state?.players;
|
|
206
|
+
if (!players)
|
|
207
|
+
return;
|
|
208
|
+
const nowSids = new Set();
|
|
209
|
+
if (typeof players.forEach === 'function') {
|
|
210
|
+
try {
|
|
211
|
+
players.forEach((p, sid) => {
|
|
212
|
+
nowSids.add(sid);
|
|
213
|
+
const name = typeof p?.displayName === 'string' ? p.displayName : '';
|
|
214
|
+
if (this.knownNames.has(sid)) {
|
|
215
|
+
// Refresh the cached name in case Colyseus updated it post-join
|
|
216
|
+
// (e.g. delayed state sync); but don't re-emit for an existing sid.
|
|
217
|
+
this.knownNames.set(sid, name);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.knownNames.set(sid, name);
|
|
221
|
+
if (sid === this.room.sessionId)
|
|
222
|
+
return; // don't announce self-join
|
|
223
|
+
const who = name || 'A player';
|
|
224
|
+
this.dispatch({
|
|
225
|
+
kind: 'system.joined',
|
|
226
|
+
from: sid,
|
|
227
|
+
displayName: name,
|
|
228
|
+
text: `${who} joined`,
|
|
229
|
+
ts: Date.now(),
|
|
230
|
+
});
|
|
196
231
|
});
|
|
197
|
-
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Iteration shouldn't throw post-snapshot, but if it does we just
|
|
235
|
+
// skip this diff cycle rather than break the chat stream.
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
198
238
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
});
|
|
239
|
+
// Anyone in knownNames but not in nowSids has left.
|
|
240
|
+
for (const sid of [...this.knownNames.keys()]) {
|
|
241
|
+
if (nowSids.has(sid))
|
|
242
|
+
continue;
|
|
243
|
+
const name = this.knownNames.get(sid) ?? '';
|
|
244
|
+
this.knownNames.delete(sid);
|
|
245
|
+
const who = name || 'A player';
|
|
246
|
+
this.dispatch({
|
|
247
|
+
kind: 'system.left',
|
|
248
|
+
from: sid,
|
|
249
|
+
displayName: name,
|
|
250
|
+
text: `${who} left`,
|
|
251
|
+
ts: Date.now(),
|
|
213
252
|
});
|
|
214
253
|
}
|
|
215
254
|
}
|