@unboxy/phaser-sdk 0.2.14 → 0.2.16
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 +16 -2
- package/dist/core/UnboxyGame.d.ts +18 -5
- package/dist/core/UnboxyGame.js +6 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/orientation.d.ts +5 -0
- package/dist/orientation.js +4 -0
- package/dist/realtime/UnboxyRoom.d.ts +40 -17
- package/dist/realtime/UnboxyRoom.js +89 -57
- package/package.json +1 -1
package/SDK-GUIDE.md
CHANGED
|
@@ -15,16 +15,28 @@ Reference for AI agents building games on the Unboxy platform. Tracks the **inst
|
|
|
15
15
|
|
|
16
16
|
| field | type | note |
|
|
17
17
|
|-------|------|------|
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
18
|
+
| `orientation` | `'portrait' \| 'landscape'` | mutually exclusive with `width`/`height`. Resolves to preset dims from `ORIENTATION_DIMENSIONS`. **Since 0.2.16.** |
|
|
19
|
+
| `width` | `number` | required if `orientation` is omitted |
|
|
20
|
+
| `height` | `number` | required if `orientation` is omitted |
|
|
20
21
|
| `scenes` | `Phaser.Scene class[]` | required |
|
|
21
22
|
| `backgroundColor` | `string` | defaults to `'#1a1a2e'` |
|
|
22
23
|
| `pixelArt` | `boolean` | defaults to `false` |
|
|
23
24
|
| `physics` | `Phaser.Types.Core.PhysicsConfig` | defaults to arcade physics, no gravity |
|
|
24
25
|
| `plugins` | `Phaser.Types.Core.PluginObject` | forwarded as-is into the Phaser `GameConfig`. **Since 0.2.9.** Earlier versions silently dropped this field. |
|
|
25
26
|
|
|
27
|
+
`orientation` and explicit `width`/`height` are a TypeScript union — pass one or the other, not both. The compiler rejects mixed usage.
|
|
28
|
+
|
|
26
29
|
Any field not listed is ignored. If you need something else (e.g. `callbacks`, custom `render` options), use plain `new Phaser.Game(config)` instead of `createUnboxyGame`.
|
|
27
30
|
|
|
31
|
+
### Orientation presets
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { ORIENTATION_DIMENSIONS } from '@unboxy/phaser-sdk';
|
|
35
|
+
// { landscape: { width: 1280, height: 720 }, portrait: { width: 720, height: 1280 } }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The orientation a game uses is decided **once at game creation** and baked into the template's `src/config.ts`. There is no runtime API to flip orientation — switching mid-development would invalidate every screen layout.
|
|
39
|
+
|
|
28
40
|
### Registering third-party Phaser plugins (e.g. rexUI)
|
|
29
41
|
|
|
30
42
|
Pass them via the `plugins` option — do NOT try to register them inside a scene's `preload`:
|
|
@@ -544,6 +556,8 @@ Don't blindly apply interpolation to everything. It's wrong for a chess piece an
|
|
|
544
556
|
|
|
545
557
|
## Changelog
|
|
546
558
|
|
|
559
|
+
- **0.2.16** — added orientation presets. `createUnboxyGame` now accepts an `orientation: 'portrait' | 'landscape'` option as an alternative to explicit `width`/`height` (TS union — pass one or the other). New exports: `Orientation` type and `ORIENTATION_DIMENSIONS` map (`landscape: 1280×720`, `portrait: 720×1280`). Lets games declare orientation once and have a single source of truth for canvas dimensions.
|
|
560
|
+
- **0.2.15** — fix: `ChatFacade.subscribeToPlayers` early-returned when `state.players` was `undefined` at construction time, which it always is on a fresh `joinOrCreate` (Colyseus delivers initial state as a separate message a few ms later). The early return meant `onStateChange` never subscribed and `system.joined` / `system.left` stayed silent in production despite the 0.2.14 callback-API fix. Now: always subscribe; treat the first state change as initial hydration (silent), subsequent changes do real diff. Three regression tests cover the deferred-hydration flow.
|
|
547
561
|
- **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.
|
|
548
562
|
- **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.
|
|
549
563
|
- **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.
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import Phaser from 'phaser';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
width: number;
|
|
5
|
-
/** Game height in pixels */
|
|
6
|
-
height: number;
|
|
2
|
+
import { type Orientation } from '../orientation.js';
|
|
3
|
+
interface UnboxyGameBaseOptions {
|
|
7
4
|
/** Phaser scene classes to register */
|
|
8
5
|
scenes: (new (...args: any[]) => Phaser.Scene)[];
|
|
9
6
|
/** Background color (default: '#1a1a2e') */
|
|
@@ -15,8 +12,24 @@ export interface UnboxyGameOptions {
|
|
|
15
12
|
/** Phaser plugin registrations (e.g. `phaser3-rex-plugins` virtual joystick) */
|
|
16
13
|
plugins?: Phaser.Types.Core.PluginObject;
|
|
17
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Either pass `orientation` (preset dims from ORIENTATION_DIMENSIONS) OR
|
|
17
|
+
* explicit `width`/`height` — not both. The TS union enforces this at
|
|
18
|
+
* compile time so games can't drift out of the orientation presets by
|
|
19
|
+
* accident.
|
|
20
|
+
*/
|
|
21
|
+
export type UnboxyGameOptions = (UnboxyGameBaseOptions & {
|
|
22
|
+
orientation: Orientation;
|
|
23
|
+
width?: never;
|
|
24
|
+
height?: never;
|
|
25
|
+
}) | (UnboxyGameBaseOptions & {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
orientation?: never;
|
|
29
|
+
});
|
|
18
30
|
/**
|
|
19
31
|
* Create an Unboxy-enhanced Phaser game instance.
|
|
20
32
|
* Includes built-in integrations: screenshot capture, preserveDrawingBuffer, etc.
|
|
21
33
|
*/
|
|
22
34
|
export declare function createUnboxyGame(options: UnboxyGameOptions): Phaser.Game;
|
|
35
|
+
export {};
|
package/dist/core/UnboxyGame.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import Phaser from 'phaser';
|
|
2
2
|
import { setupScreenshotListener } from '../screenshot/ScreenshotManager.js';
|
|
3
3
|
import { setupRecordingListener } from '../recording/RecordingManager.js';
|
|
4
|
+
import { ORIENTATION_DIMENSIONS } from '../orientation.js';
|
|
4
5
|
/**
|
|
5
6
|
* Create an Unboxy-enhanced Phaser game instance.
|
|
6
7
|
* Includes built-in integrations: screenshot capture, preserveDrawingBuffer, etc.
|
|
7
8
|
*/
|
|
8
9
|
export function createUnboxyGame(options) {
|
|
10
|
+
const { width, height } = 'orientation' in options && options.orientation
|
|
11
|
+
? ORIENTATION_DIMENSIONS[options.orientation]
|
|
12
|
+
: { width: options.width, height: options.height };
|
|
9
13
|
const config = {
|
|
10
14
|
type: Phaser.AUTO,
|
|
11
15
|
backgroundColor: options.backgroundColor ?? '#1a1a2e',
|
|
12
16
|
scale: {
|
|
13
17
|
mode: Phaser.Scale.FIT,
|
|
14
18
|
autoCenter: Phaser.Scale.CENTER_BOTH,
|
|
15
|
-
width
|
|
16
|
-
height
|
|
19
|
+
width,
|
|
20
|
+
height,
|
|
17
21
|
},
|
|
18
22
|
render: {
|
|
19
23
|
preserveDrawingBuffer: true,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { createUnboxyGame } from './core/UnboxyGame.js';
|
|
2
2
|
export type { UnboxyGameOptions } from './core/UnboxyGame.js';
|
|
3
3
|
export { UnboxyScene } from './core/UnboxyScene.js';
|
|
4
|
+
export { ORIENTATION_DIMENSIONS } from './orientation.js';
|
|
5
|
+
export type { Orientation } from './orientation.js';
|
|
4
6
|
export { setupScreenshotListener, takeScreenshot } from './screenshot/ScreenshotManager.js';
|
|
5
7
|
export { setupRecordingListener } from './recording/RecordingManager.js';
|
|
6
8
|
export { Unboxy } from './core/Unboxy.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Core
|
|
2
2
|
export { createUnboxyGame } from './core/UnboxyGame.js';
|
|
3
3
|
export { UnboxyScene } from './core/UnboxyScene.js';
|
|
4
|
+
export { ORIENTATION_DIMENSIONS } from './orientation.js';
|
|
4
5
|
// Screenshot
|
|
5
6
|
export { setupScreenshotListener, takeScreenshot } from './screenshot/ScreenshotManager.js';
|
|
6
7
|
// Recording
|
|
@@ -84,12 +84,18 @@ export declare class ChatFacade {
|
|
|
84
84
|
private readonly handlers;
|
|
85
85
|
private remoteOff;
|
|
86
86
|
/** Sids known to this facade, mapped to the displayName they had at the
|
|
87
|
-
* time we last observed them. Populated at construction
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
87
|
+
* time we last observed them. Populated either at construction (if state
|
|
88
|
+
* was already synced) or on the first `onStateChange` (initial hydration
|
|
89
|
+
* — silent, no system messages). Subsequent state changes diff against
|
|
90
|
+
* this map to emit `system.joined` / `system.left`. We track displayName
|
|
91
|
+
* here because by the time a player is detected as "left" they're already
|
|
92
|
+
* gone from `state.players` and we can't read it from there. */
|
|
92
93
|
private readonly knownNames;
|
|
94
|
+
/** Becomes true the first time we observe a usable `state.players` map.
|
|
95
|
+
* The very first observation is treated as "initial state arrived" and
|
|
96
|
+
* must NOT fire `system.joined` for the players already in the room when
|
|
97
|
+
* the local user joined — only subsequent diffs are real lifecycle events. */
|
|
98
|
+
private hydrated;
|
|
93
99
|
constructor(room: Room<unknown>);
|
|
94
100
|
/**
|
|
95
101
|
* Send a chat line. Empty-after-trim input is dropped silently. Text longer
|
|
@@ -111,26 +117,43 @@ export declare class ChatFacade {
|
|
|
111
117
|
*/
|
|
112
118
|
onMessage(handler: (msg: ChatMessage) => void): Unsubscribe;
|
|
113
119
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
120
|
+
* Hook `onStateChange` to detect joins / leaves and try an eager snapshot
|
|
121
|
+
* if `state.players` is already populated.
|
|
122
|
+
*
|
|
123
|
+
* **The onStateChange subscription must always be set up, even when
|
|
124
|
+
* `state.players` is `undefined` at construction.** Colyseus 0.16 (and
|
|
125
|
+
* earlier) deliver the initial state as a *separate* message that arrives
|
|
126
|
+
* a few ms after `client.joinOrCreate` resolves — so right when UnboxyRoom
|
|
127
|
+
* is constructed, `room.state.players` is typically `undefined`. An earlier
|
|
128
|
+
* 0.2.14 implementation early-returned in that case and never subscribed,
|
|
129
|
+
* which meant the diff machinery never armed and BOTH `system.joined` /
|
|
130
|
+
* `system.left` silently dead-stopped in production (Blokus chat game,
|
|
131
|
+
* 2026-04-27).
|
|
132
|
+
*
|
|
133
|
+
* Eager snapshot if `state.players` is already there sets `hydrated=true`
|
|
134
|
+
* so the first state change does a real diff. Otherwise the first state
|
|
135
|
+
* change is treated as initial hydration: populate `knownNames` silently,
|
|
136
|
+
* skip system messages, set `hydrated=true`. Subsequent state changes do
|
|
137
|
+
* the real lifecycle diff.
|
|
119
138
|
*
|
|
120
139
|
* Why `onStateChange` and not `MapSchema.onAdd` / `.onRemove`: Colyseus
|
|
121
140
|
* 0.16 dropped those instance methods in favour of a separate
|
|
122
141
|
* `getStateCallbacks(room)` proxy API. Hooking `onStateChange` and diffing
|
|
123
142
|
* `players` keys ourselves works the same in any Colyseus version, keeps
|
|
124
143
|
* 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).
|
|
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.
|
|
144
|
+
* of work per state change for typical room sizes (max 16 players).
|
|
130
145
|
*/
|
|
131
146
|
private subscribeToPlayers;
|
|
132
|
-
/**
|
|
133
|
-
*
|
|
147
|
+
/** Populate `knownNames` from `state.players` if it's already available
|
|
148
|
+
* and mark hydrated. Silent — never emits system messages. A successful
|
|
149
|
+
* `forEach` (even iterating zero items) means the schema is synced and
|
|
150
|
+
* this is "what the room looked like when we joined" — safe to hydrate. */
|
|
151
|
+
private tryEagerSnapshot;
|
|
152
|
+
/** Compare current `players` membership against `knownNames`. On the very
|
|
153
|
+
* first invocation (post-construction) when `hydrated=false`, this is
|
|
154
|
+
* the initial-hydration call: populate `knownNames` silently and bail.
|
|
155
|
+
* Subsequent invocations diff against `knownNames` and emit
|
|
156
|
+
* `system.joined` for new sids, `system.left` for vanished sids. */
|
|
134
157
|
private diffPlayers;
|
|
135
158
|
private dispatch;
|
|
136
159
|
}
|
|
@@ -77,12 +77,18 @@ export class ChatFacade {
|
|
|
77
77
|
this.handlers = new Set();
|
|
78
78
|
this.remoteOff = null;
|
|
79
79
|
/** Sids known to this facade, mapped to the displayName they had at the
|
|
80
|
-
* time we last observed them. Populated at construction
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
80
|
+
* time we last observed them. Populated either at construction (if state
|
|
81
|
+
* was already synced) or on the first `onStateChange` (initial hydration
|
|
82
|
+
* — silent, no system messages). Subsequent state changes diff against
|
|
83
|
+
* this map to emit `system.joined` / `system.left`. We track displayName
|
|
84
|
+
* here because by the time a player is detected as "left" they're already
|
|
85
|
+
* gone from `state.players` and we can't read it from there. */
|
|
85
86
|
this.knownNames = new Map();
|
|
87
|
+
/** Becomes true the first time we observe a usable `state.players` map.
|
|
88
|
+
* The very first observation is treated as "initial state arrived" and
|
|
89
|
+
* must NOT fire `system.joined` for the players already in the room when
|
|
90
|
+
* the local user joined — only subsequent diffs are real lifecycle events. */
|
|
91
|
+
this.hydrated = false;
|
|
86
92
|
this.subscribeToPlayers();
|
|
87
93
|
}
|
|
88
94
|
/**
|
|
@@ -158,83 +164,109 @@ export class ChatFacade {
|
|
|
158
164
|
};
|
|
159
165
|
}
|
|
160
166
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
167
|
+
* Hook `onStateChange` to detect joins / leaves and try an eager snapshot
|
|
168
|
+
* if `state.players` is already populated.
|
|
169
|
+
*
|
|
170
|
+
* **The onStateChange subscription must always be set up, even when
|
|
171
|
+
* `state.players` is `undefined` at construction.** Colyseus 0.16 (and
|
|
172
|
+
* earlier) deliver the initial state as a *separate* message that arrives
|
|
173
|
+
* a few ms after `client.joinOrCreate` resolves — so right when UnboxyRoom
|
|
174
|
+
* is constructed, `room.state.players` is typically `undefined`. An earlier
|
|
175
|
+
* 0.2.14 implementation early-returned in that case and never subscribed,
|
|
176
|
+
* which meant the diff machinery never armed and BOTH `system.joined` /
|
|
177
|
+
* `system.left` silently dead-stopped in production (Blokus chat game,
|
|
178
|
+
* 2026-04-27).
|
|
179
|
+
*
|
|
180
|
+
* Eager snapshot if `state.players` is already there sets `hydrated=true`
|
|
181
|
+
* so the first state change does a real diff. Otherwise the first state
|
|
182
|
+
* change is treated as initial hydration: populate `knownNames` silently,
|
|
183
|
+
* skip system messages, set `hydrated=true`. Subsequent state changes do
|
|
184
|
+
* the real lifecycle diff.
|
|
166
185
|
*
|
|
167
186
|
* Why `onStateChange` and not `MapSchema.onAdd` / `.onRemove`: Colyseus
|
|
168
187
|
* 0.16 dropped those instance methods in favour of a separate
|
|
169
188
|
* `getStateCallbacks(room)` proxy API. Hooking `onStateChange` and diffing
|
|
170
189
|
* `players` keys ourselves works the same in any Colyseus version, keeps
|
|
171
190
|
* 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).
|
|
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.
|
|
191
|
+
* of work per state change for typical room sizes (max 16 players).
|
|
177
192
|
*/
|
|
178
193
|
subscribeToPlayers() {
|
|
194
|
+
// Always subscribe — regardless of whether `state.players` is populated yet.
|
|
195
|
+
this.room.onStateChange(() => this.diffPlayers());
|
|
196
|
+
// Best-effort eager snapshot. If state is already synced (rare — usually
|
|
197
|
+
// only in tests or on a reconnect to a hot room), this lets us mark
|
|
198
|
+
// hydrated immediately and treat the first state change as a real diff.
|
|
199
|
+
this.tryEagerSnapshot();
|
|
200
|
+
}
|
|
201
|
+
/** Populate `knownNames` from `state.players` if it's already available
|
|
202
|
+
* and mark hydrated. Silent — never emits system messages. A successful
|
|
203
|
+
* `forEach` (even iterating zero items) means the schema is synced and
|
|
204
|
+
* this is "what the room looked like when we joined" — safe to hydrate. */
|
|
205
|
+
tryEagerSnapshot() {
|
|
179
206
|
const players = this.room.state?.players;
|
|
180
|
-
if (!players)
|
|
207
|
+
if (!players || typeof players.forEach !== 'function')
|
|
181
208
|
return;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// which means we'll emit "joined" for the initial hydration. That's
|
|
192
|
-
// a degraded but non-broken behavior — the chat log gets an extra
|
|
193
|
-
// line per existing player. Log once for visibility.
|
|
194
|
-
console.warn('[unboxy.chat] could not snapshot players on subscribe');
|
|
195
|
-
}
|
|
209
|
+
try {
|
|
210
|
+
players.forEach((p, sid) => {
|
|
211
|
+
const name = typeof p?.displayName === 'string' ? p.displayName : '';
|
|
212
|
+
this.knownNames.set(sid, name);
|
|
213
|
+
});
|
|
214
|
+
this.hydrated = true;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// State not iterable yet — leave it for the first onStateChange.
|
|
196
218
|
}
|
|
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
219
|
}
|
|
201
|
-
/** Compare current `players` membership against `knownNames
|
|
202
|
-
*
|
|
220
|
+
/** Compare current `players` membership against `knownNames`. On the very
|
|
221
|
+
* first invocation (post-construction) when `hydrated=false`, this is
|
|
222
|
+
* the initial-hydration call: populate `knownNames` silently and bail.
|
|
223
|
+
* Subsequent invocations diff against `knownNames` and emit
|
|
224
|
+
* `system.joined` for new sids, `system.left` for vanished sids. */
|
|
203
225
|
diffPlayers() {
|
|
204
226
|
const state = this.room.state;
|
|
205
227
|
const players = state?.players;
|
|
206
|
-
if (!players)
|
|
228
|
+
if (!players || typeof players.forEach !== 'function')
|
|
207
229
|
return;
|
|
208
|
-
|
|
209
|
-
|
|
230
|
+
if (!this.hydrated) {
|
|
231
|
+
// Initial hydration — silently absorb whoever's in the room when we
|
|
232
|
+
// joined. Don't say "X joined" for everyone already there.
|
|
210
233
|
try {
|
|
211
234
|
players.forEach((p, sid) => {
|
|
212
|
-
nowSids.add(sid);
|
|
213
235
|
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
236
|
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
|
-
});
|
|
231
237
|
});
|
|
232
238
|
}
|
|
233
239
|
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
240
|
return;
|
|
237
241
|
}
|
|
242
|
+
this.hydrated = true;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const nowSids = new Set();
|
|
246
|
+
try {
|
|
247
|
+
players.forEach((p, sid) => {
|
|
248
|
+
nowSids.add(sid);
|
|
249
|
+
const name = typeof p?.displayName === 'string' ? p.displayName : '';
|
|
250
|
+
if (this.knownNames.has(sid)) {
|
|
251
|
+
// Refresh cached name in case Colyseus updated it post-join.
|
|
252
|
+
this.knownNames.set(sid, name);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.knownNames.set(sid, name);
|
|
256
|
+
if (sid === this.room.sessionId)
|
|
257
|
+
return; // don't announce self-join
|
|
258
|
+
const who = name || 'A player';
|
|
259
|
+
this.dispatch({
|
|
260
|
+
kind: 'system.joined',
|
|
261
|
+
from: sid,
|
|
262
|
+
displayName: name,
|
|
263
|
+
text: `${who} joined`,
|
|
264
|
+
ts: Date.now(),
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return;
|
|
238
270
|
}
|
|
239
271
|
// Anyone in knownNames but not in nowSids has left.
|
|
240
272
|
for (const sid of [...this.knownNames.keys()]) {
|