@smoregg/sdk 2.2.0 → 2.3.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 CHANGED
@@ -1,6 +1,18 @@
1
1
  # @smoregg/sdk
2
2
 
3
- S'MORE Game SDK - Build party games with React for the S'MORE platform.
3
+ SDK for building multiplayer party games on the S'MORE platform.
4
+
5
+ **v2.3.0** | TypeScript | Zero runtime dependencies | ESM, CJS, UMD
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ S'MORE is a multiplayer party game platform where a shared display (the **Screen**, typically a TV or computer) runs the game while each player uses their phone as a **Controller** -- an input device and personal display. Think Jackbox-style games with full developer control.
12
+
13
+ This SDK provides type-safe APIs for communication between the Screen and Controllers. Define your game's event types once in a shared interface, and get full compile-time checking on every `send()`, `broadcast()`, and `on()` call. The SDK handles connection management, reconnection, lifecycle events, and message delivery.
14
+
15
+ The core architectural principle is the **Stateless Controller Pattern**: Controllers are display + input devices only. The Screen holds all game state and is the single source of truth. When a player reconnects, the Screen simply re-pushes the current view -- no state synchronization needed.
4
16
 
5
17
  ## Installation
6
18
 
@@ -14,177 +26,415 @@ yarn add @smoregg/sdk
14
26
 
15
27
  ## Quick Start
16
28
 
17
- ### Screen Game (TV/Display)
29
+ ### 1. Define Your Events
30
+
31
+ Create a shared event map that both Screen and Controller will use:
32
+
33
+ ```typescript
34
+ // events.ts (shared between Screen and Controller)
35
+ interface GameEvents {
36
+ // Screen -> Controller (view state)
37
+ 'game-state': { phase: string; score: number };
38
+ // Controller -> Screen (input)
39
+ 'tap': { timestamp: number };
40
+ }
41
+ ```
42
+
43
+ ### 2. Screen (TV / Shared Display)
18
44
 
19
- ```tsx
45
+ ```typescript
20
46
  import { createScreen } from '@smoregg/sdk';
21
47
 
22
- function MyGame() {
23
- const screen = createScreen({
24
- gameId: 'my-game',
25
- listeners: {
26
- tap: (playerIndex, data) => {
27
- console.log(`Player ${playerIndex} tapped!`);
28
- screen.broadcast('score-update', { playerIndex, score: 10 });
29
- }
30
- }
31
- });
48
+ const screen = createScreen<GameEvents>();
32
49
 
33
- return <div>Game UI</div>;
34
- }
50
+ // Listen for player input
51
+ screen.on('tap', (playerIndex, data) => {
52
+ console.log(`Player ${playerIndex} tapped at ${data.timestamp}`);
53
+ // Update game state and push to all controllers
54
+ screen.broadcast('game-state', { phase: 'playing', score: 10 });
55
+ });
56
+
57
+ // Re-push view to reconnecting players
58
+ screen.onControllerReconnect((playerIndex) => {
59
+ screen.sendToController(playerIndex, 'game-state', getCurrentState());
60
+ });
61
+
62
+ await screen.ready;
35
63
  ```
36
64
 
37
- ### Controller App (Mobile)
65
+ ### 3. Controller (Phone)
38
66
 
39
- ```tsx
67
+ ```typescript
40
68
  import { createController } from '@smoregg/sdk';
41
69
 
42
- function MyController() {
43
- const controller = createController({
44
- gameId: 'my-game',
45
- listeners: {
46
- 'state-update': (state) => {
47
- console.log('Game state:', state);
48
- }
49
- }
50
- });
70
+ const controller = createController<GameEvents>();
51
71
 
52
- return (
53
- <button onClick={() => controller.send('tap', {})}>
54
- TAP
55
- </button>
56
- );
72
+ // Render what Screen sends (stateless -- no local game state)
73
+ controller.on('game-state', (data) => {
74
+ renderUI(data.phase, data.score);
75
+ });
76
+
77
+ // Send input to Screen
78
+ function handleTap() {
79
+ controller.send('tap', { timestamp: Date.now() });
57
80
  }
81
+
82
+ await controller.ready;
58
83
  ```
59
84
 
60
- ## Features
85
+ ## Architecture
61
86
 
62
- - **Screen/Controller Communication**: Simple event-based messaging
63
- - **TypeScript Support**: Full type definitions included
64
- - **Testing Utilities**: Mock Screen and Controller for unit testing
65
- - **Multiple Formats**: ESM, CJS, and UMD builds
66
- - **Zero Dependencies**: Peer dependencies only (React, Socket.IO)
87
+ ```
88
+ ┌─────────┐ events ┌─────────┐ relay ┌──────────────┐
89
+ │ Screen │ <----------> │ Server │ <--------> Controller
90
+ │ (TV) │ │ (relay) │ │ (Phone) │
91
+ │ │ │ │ │ │
92
+ │ Game │ broadcast │ No game │ on() │ Display only │
93
+ │ Logic │ -----------> │ logic │ ---------> │ + Input │
94
+ │ State │ │ │ │ │
95
+ │ Source │ sendToCtrl │ │ send() │ No game │
96
+ │ of Truth │ -----------> │ │ <--------- │ state │
97
+ └─────────┘ └─────────┘ └──────────────┘
98
+ ```
99
+
100
+ **Data flow:**
101
+
102
+ - **Controller to Screen:** Input only via `controller.send()`
103
+ - **Screen to all Controllers:** View state via `screen.broadcast()`
104
+ - **Screen to one Controller:** Targeted view state via `screen.sendToController()`
105
+ - **Reconnection:** Screen re-pushes view in `onControllerReconnect` callback
106
+
107
+ The server is a stateless relay -- it forwards messages without game logic. All game state lives on the Screen.
67
108
 
68
109
  ## API Reference
69
110
 
70
- ### createScreen()
111
+ ### Screen
71
112
 
72
- Create a screen-side game instance.
113
+ #### Creating a Screen
73
114
 
74
115
  ```typescript
75
116
  import { createScreen } from '@smoregg/sdk';
76
117
 
77
- const screen = createScreen({
78
- gameId: string;
79
- listeners?: Record<string, (playerIndex: number, data: any) => void>;
80
- });
118
+ const screen = createScreen<MyEvents>(config?);
119
+ ```
81
120
 
82
- // Methods
83
- screen.broadcast(event: string, data: any): void
84
- screen.sendToController(playerIndex: number, event: string, data: any): void
85
- screen.gameOver(results: GameResults): void
86
- screen.on(event: string, callback: Function): void
87
- screen.off(event: string, callback: Function): void
121
+ #### Config Options
88
122
 
89
- // Properties
90
- screen.controllers: Array<{ playerIndex: number; nickname: string; connected: boolean }>
91
- screen.myIndex: number
92
- ```
123
+ | Option | Type | Default | Description |
124
+ |--------|------|---------|-------------|
125
+ | `debug` | `boolean \| DebugOptions` | `false` | Enable debug logging |
126
+ | `parentOrigin` | `string` | `'*'` | Parent window origin for message validation |
127
+ | `timeout` | `number` | `10000` | Connection timeout in milliseconds |
128
+ | `autoReady` | `boolean` | `true` | Automatically signal ready after initialization |
93
129
 
94
- ### createController()
130
+ #### Properties
95
131
 
96
- Create a controller-side game instance.
132
+ | Property | Type | Description |
133
+ |----------|------|-------------|
134
+ | `controllers` | `readonly ControllerInfo[]` | All connected controllers (shallow copy per access) |
135
+ | `roomCode` | `string` | Room code for this game session |
136
+ | `isReady` | `boolean` | Whether the screen is initialized |
137
+ | `isDestroyed` | `boolean` | Whether the screen has been destroyed |
138
+ | `isConnected` | `boolean` | Whether the connection is active |
139
+ | `ready` | `Promise<void>` | Resolves when the screen is ready |
97
140
 
98
- ```typescript
99
- import { createController } from '@smoregg/sdk';
141
+ #### Communication
100
142
 
101
- const controller = createController({
102
- gameId: string;
103
- listeners?: Record<string, (data: any) => void>;
104
- });
143
+ | Method | Description |
144
+ |--------|-------------|
145
+ | `broadcast(event, data)` | Send to all controllers. Rate limit: 60/sec (shared with `sendToController`). Max payload: 64KB. |
146
+ | `sendToController(playerIndex, event, data)` | Send to one controller. Shares the 60/sec rate limit with `broadcast`. |
147
+ | `gameOver(results?)` | End the game. Accepts optional `GameResults` with scores, winner, rankings. |
148
+ | `signalReady()` | Signal ready to the server. Auto-called if `autoReady` is `true`. |
105
149
 
106
- // Methods
107
- controller.send(event: string, data: any): void
108
- controller.on(event: string, callback: Function): void
109
- controller.off(event: string, callback: Function): void
150
+ #### Lifecycle Callbacks
110
151
 
111
- // Properties
112
- controller.myIndex: number
113
- controller.isLeader: boolean
114
- ```
152
+ All lifecycle methods return an unsubscribe function.
115
153
 
116
- ## Backward Compatibility
154
+ | Method | Callback Signature | Description |
155
+ |--------|--------------------|-------------|
156
+ | `onAllReady(cb)` | `() => void` | All participants are ready. Fires immediately if already ready. |
157
+ | `onControllerJoin(cb)` | `(playerIndex, info) => void` | A player joined the room |
158
+ | `onControllerLeave(cb)` | `(playerIndex) => void` | A player left the room |
159
+ | `onControllerDisconnect(cb)` | `(playerIndex) => void` | A player temporarily disconnected |
160
+ | `onControllerReconnect(cb)` | `(playerIndex, info) => void` | A player reconnected |
161
+ | `onCharacterUpdated(cb)` | `(playerIndex, appearance) => void` | A player's character appearance changed |
162
+ | `onError(cb)` | `(error: SmoreError) => void` | An SDK error occurred |
163
+ | `onConnectionChange(cb)` | `(connected: boolean) => void` | Connection status changed |
117
164
 
118
- The old type aliases are still available:
165
+ #### Event Subscription
166
+
167
+ | Method | Description |
168
+ |--------|-------------|
169
+ | `on(event, handler)` | Subscribe to an event. Handler receives `(playerIndex, data)`. Returns unsubscribe function. |
170
+ | `once(event, handler)` | Subscribe once. Auto-removes after first call. |
171
+ | `off(event, handler?)` | Remove a specific handler, or all handlers for an event. |
172
+ | `removeAllListeners(event?)` | Remove all user event listeners, or all for a specific event. |
173
+
174
+ #### Utilities
175
+
176
+ | Method | Description |
177
+ |--------|-------------|
178
+ | `getController(playerIndex)` | Get a `ControllerInfo` by player index, or `undefined` |
179
+ | `getControllerCount()` | Number of currently connected controllers |
180
+ | `destroy()` | Clean up all resources and disconnect |
181
+
182
+ ---
183
+
184
+ ### Controller
185
+
186
+ #### Creating a Controller
119
187
 
120
188
  ```typescript
121
- // Old names (deprecated)
122
- import type { SmoreHost, SmorePlayer } from '@smoregg/sdk';
189
+ import { createController } from '@smoregg/sdk';
123
190
 
124
- // New names (preferred)
125
- import type { SmoreScreen, SmoreController } from '@smoregg/sdk';
191
+ const controller = createController<MyEvents>(config?);
126
192
  ```
127
193
 
128
- ## Testing
194
+ Config options are the same as Screen.
129
195
 
130
- The SDK provides comprehensive testing utilities for unit testing your game logic:
196
+ #### Properties
131
197
 
132
- ```typescript
133
- import { createMockScreen, createMockController } from '@smoregg/sdk';
198
+ | Property | Type | Description |
199
+ |----------|------|-------------|
200
+ | `myPlayerIndex` | `number` | This player's index (0, 1, 2, ...) |
201
+ | `me` | `ControllerInfo \| undefined` | This player's info |
202
+ | `roomCode` | `string` | Room code for this game session |
203
+ | `controllers` | `readonly ControllerInfo[]` | All known controllers in the room |
204
+ | `isReady` | `boolean` | Whether the controller is initialized |
205
+ | `isDestroyed` | `boolean` | Whether the controller has been destroyed |
206
+ | `isConnected` | `boolean` | Whether the connection is active |
207
+ | `ready` | `Promise<void>` | Resolves when the controller is ready |
134
208
 
135
- // Create mock screen with players
136
- const screen = createMockScreen<MyEvents>({
137
- controllers: [
138
- { playerIndex: 0, nickname: 'Player 1', connected: true },
139
- ],
140
- });
209
+ #### Communication
210
+
211
+ | Method | Description |
212
+ |--------|-------------|
213
+ | `send(event, data)` | Send to Screen. Rate limit: 60/sec. Max payload: 64KB. |
214
+ | `signalReady()` | Signal ready to the server. Auto-called if `autoReady` is `true`. |
215
+
216
+ Controller has no `broadcast()` -- all communication goes through Screen. Controller-to-Controller messaging is not supported; route through Screen instead.
141
217
 
142
- // Simulate player input
143
- screen.simulateEvent(0, 'tap', { x: 100, y: 200 });
218
+ #### Lifecycle Callbacks
144
219
 
145
- // Check what was broadcast
146
- expect(screen.getBroadcasts()).toContainEqual({
147
- event: 'score-update',
148
- data: { scores: { 0: 10 } },
220
+ Same as Screen, plus:
221
+
222
+ | Method | Callback Signature | Description |
223
+ |--------|--------------------|-------------|
224
+ | `onGameOver(cb)` | `(results?) => void` | The game has ended (Screen called `gameOver()`) |
225
+
226
+ #### Event Subscription
227
+
228
+ Same API as Screen. Handler receives `(data)` only -- no `playerIndex` parameter.
229
+
230
+ ```typescript
231
+ controller.on('game-state', (data) => {
232
+ // data is type-safe: { phase: string; score: number }
233
+ renderUI(data.phase, data.score);
149
234
  });
150
235
  ```
151
236
 
152
- See [docs/testing.md](./docs/testing.md) for the full testing guide.
237
+ #### Utilities
238
+
239
+ | Method | Description |
240
+ |--------|-------------|
241
+ | `getController(playerIndex)` | Get a `ControllerInfo` by player index, or `undefined` |
242
+ | `getControllerCount()` | Number of currently connected controllers |
243
+ | `destroy()` | Clean up all resources and disconnect |
153
244
 
154
- ## Types
245
+ ---
155
246
 
156
- All types are exported from the main package:
247
+ ### Types
157
248
 
158
249
  ```typescript
159
250
  import type {
160
- Player,
161
- PlayerDTO,
251
+ EventMap,
252
+ ControllerInfo,
253
+ GameResults,
254
+ SmoreError,
255
+ SmoreErrorCode,
162
256
  CharacterAppearance,
163
- GameMetadata,
164
- GameState,
165
- InputCallback
257
+ PlayerIndex,
258
+ Screen,
259
+ Controller,
166
260
  } from '@smoregg/sdk';
261
+
262
+ import { SmoreSDKError, LifecycleEvent } from '@smoregg/sdk';
167
263
  ```
168
264
 
169
- ## Building
265
+ #### ControllerInfo
170
266
 
171
- ```bash
172
- # Development
173
- pnpm typecheck
267
+ ```typescript
268
+ interface ControllerInfo {
269
+ readonly playerIndex: number;
270
+ readonly nickname: string;
271
+ readonly connected: boolean;
272
+ readonly appearance?: CharacterAppearance | null;
273
+ }
274
+ ```
275
+
276
+ #### GameResults
174
277
 
175
- # Build all formats (ESM, CJS, UMD)
176
- pnpm build
278
+ ```typescript
279
+ interface GameResults {
280
+ scores?: Record<number, number>;
281
+ winner?: number;
282
+ rankings?: number[];
283
+ custom?: Record<string, unknown>;
284
+ }
285
+ ```
177
286
 
178
- # Clean
179
- pnpm clean
287
+ #### SmoreSDKError
288
+
289
+ ```typescript
290
+ class SmoreSDKError extends Error {
291
+ readonly code: SmoreErrorCode;
292
+ readonly cause?: Error;
293
+ readonly details?: Record<string, unknown>;
294
+ }
180
295
  ```
181
296
 
182
- ## Publishing
297
+ **Error codes:** `TIMEOUT`, `NOT_READY`, `DESTROYED`, `INVALID_EVENT`, `INVALID_PLAYER`, `CONNECTION_LOST`, `INIT_FAILED`, `RATE_LIMITED`, `PAYLOAD_TOO_LARGE`, `UNKNOWN`
183
298
 
184
- ```bash
185
- pnpm publish:npm
299
+ #### LifecycleEvent Constants
300
+
301
+ Subscribe to lifecycle events via `on()` using `$`-prefixed constants:
302
+
303
+ ```typescript
304
+ import { LifecycleEvent } from '@smoregg/sdk';
305
+
306
+ // These are equivalent:
307
+ screen.onControllerJoin((playerIndex, info) => { /* ... */ });
308
+ screen.on(LifecycleEvent.CONTROLLER_JOIN, (playerIndex, info) => { /* ... */ });
309
+ ```
310
+
311
+ Available constants: `ALL_READY`, `CONTROLLER_JOIN`, `CONTROLLER_LEAVE`, `CONTROLLER_DISCONNECT`, `CONTROLLER_RECONNECT`, `CHARACTER_UPDATED`, `ERROR`, `GAME_OVER`, `CONNECTION_CHANGE`
312
+
313
+ #### EventMap
314
+
315
+ Event data values must be plain objects, not primitives. The fields `playerIndex` and `targetPlayerIndex` are reserved by the SDK.
316
+
317
+ ```typescript
318
+ // Good
319
+ interface MyEvents {
320
+ 'tap': { x: number; y: number };
321
+ 'answer': { choice: number };
322
+ }
323
+
324
+ // Bad -- primitives are not allowed
325
+ interface MyEvents {
326
+ 'tap': number; // Will break type safety
327
+ 'answer': string; // Use { value: string } instead
328
+ }
329
+ ```
330
+
331
+ ## Testing
332
+
333
+ Import test utilities from `@smoregg/sdk/testing`:
334
+
335
+ ```typescript
336
+ import { createMockScreen, createMockController } from '@smoregg/sdk/testing';
337
+ ```
338
+
339
+ ### Example Test
340
+
341
+ ```typescript
342
+ import { describe, it, expect } from 'vitest';
343
+ import { createMockScreen } from '@smoregg/sdk/testing';
344
+
345
+ interface GameEvents {
346
+ 'tap': { x: number; y: number };
347
+ 'score-update': { scores: Record<number, number> };
348
+ }
349
+
350
+ describe('My Game', () => {
351
+ it('broadcasts score on tap', () => {
352
+ const screen = createMockScreen<GameEvents>({
353
+ controllers: [
354
+ { playerIndex: 0, nickname: 'Alice', connected: true },
355
+ ],
356
+ });
357
+ screen.triggerReady();
358
+
359
+ // Register game logic
360
+ screen.on('tap', (playerIndex, data) => {
361
+ screen.broadcast('score-update', { scores: { [playerIndex]: 10 } });
362
+ });
363
+
364
+ // Simulate player input
365
+ screen.simulateEvent(0, 'tap', { x: 100, y: 200 });
366
+
367
+ // Assert game logic response
368
+ const broadcasts = screen.getBroadcasts();
369
+ expect(broadcasts).toHaveLength(1);
370
+ expect(broadcasts[0]).toEqual({
371
+ event: 'score-update',
372
+ data: { scores: { 0: 10 } },
373
+ });
374
+ });
375
+ });
186
376
  ```
187
377
 
378
+ ### MockScreen Methods
379
+
380
+ | Method | Description |
381
+ |--------|-------------|
382
+ | `triggerReady()` | Manually trigger the ready state (synchronous) |
383
+ | `simulateEvent(playerIndex, event, data)` | Simulate a controller sending an event |
384
+ | `simulateControllerJoin(info)` | Simulate a player joining |
385
+ | `simulateControllerLeave(playerIndex)` | Simulate a player leaving |
386
+ | `simulateControllerDisconnect(playerIndex)` | Simulate a player disconnecting |
387
+ | `simulateControllerReconnect(playerIndex)` | Simulate a player reconnecting |
388
+ | `simulateAllReady()` | Trigger the all-ready event |
389
+ | `simulateCharacterUpdate(playerIndex, appearance)` | Simulate a character appearance change |
390
+ | `simulateConnectionChange(connected)` | Simulate connection status change |
391
+ | `simulateError(error)` | Simulate an error event |
392
+ | `getBroadcasts()` | Get all recorded `broadcast()` calls |
393
+ | `getSentToController(playerIndex)` | Get recorded `sendToController()` calls for a player |
394
+ | `getAllSentToController()` | Get all recorded `sendToController()` calls |
395
+ | `clearRecordedEvents()` | Clear all recorded broadcasts and sends |
396
+
397
+ ### MockController Methods
398
+
399
+ | Method | Description |
400
+ |--------|-------------|
401
+ | `triggerReady()` | Manually trigger the ready state (synchronous) |
402
+ | `simulateEvent(event, data)` | Simulate the Screen sending an event |
403
+ | `simulateGameOver(results?)` | Simulate the game ending |
404
+ | `simulatePlayerJoin(playerIndex, info)` | Simulate a player joining |
405
+ | `simulatePlayerLeave(playerIndex)` | Simulate a player leaving |
406
+ | `simulatePlayerDisconnect(playerIndex)` | Simulate a player disconnecting |
407
+ | `simulatePlayerReconnect(playerIndex, info)` | Simulate a player reconnecting |
408
+ | `simulateAllReady()` | Trigger the all-ready event |
409
+ | `simulateCharacterUpdate(playerIndex, appearance)` | Simulate a character appearance change |
410
+ | `simulateConnectionChange(connected)` | Simulate connection status change |
411
+ | `simulateError(error)` | Simulate an error event |
412
+ | `getSentEvents()` | Get all recorded `send()` calls |
413
+ | `clearRecordedEvents()` | Clear all recorded sends |
414
+
415
+ **Note:** Default `autoReady` is `false` in mocks. Use `triggerReady()` for synchronous test control. If you pass `autoReady: true`, ready fires asynchronously on the next tick.
416
+
417
+ ## Event Naming Rules
418
+
419
+ | Rule | Example |
420
+ |------|---------|
421
+ | Must start with a letter | `tap` (valid), `123tap` (invalid) |
422
+ | Letters, numbers, hyphens, underscores only | `player-move` (valid), `player.move` (invalid) |
423
+ | No colons | `my-event` (valid), `my:event` (invalid, reserved for platform) |
424
+ | Max 128 characters | - |
425
+
426
+ ## Limits and Constraints
427
+
428
+ | Constraint | Value |
429
+ |------------|-------|
430
+ | Rate limit | 60 events/sec per socket (shared across all send methods) |
431
+ | Max payload | 64KB per event |
432
+ | Message ordering | Guaranteed for a single sender |
433
+ | Event data | Must be objects, not primitives |
434
+ | Reserved fields | `playerIndex` and `targetPlayerIndex` in event data |
435
+
436
+ Events exceeding the rate limit or payload size are silently dropped by the server.
437
+
188
438
  ## License
189
439
 
190
- MIT (C) S'MORE Team
440
+ MIT
@@ -23,8 +23,6 @@ class ControllerImpl {
23
23
  // Maps user-facing handler -> transport wrappedHandler for proper cleanup in on()/off()
24
24
  handlerToTransport = /* @__PURE__ */ new Map();
25
25
  _controllers = [];
26
- _customStates = /* @__PURE__ */ new Map();
27
- _stateChangeListeners = /* @__PURE__ */ new Set();
28
26
  // Pending handlers registered via on() before transport is ready
29
27
  _pendingHandlers = [];
30
28
  // Unified lifecycle listener map (supports both onXxx() and on('$xxx') patterns)
@@ -183,10 +181,6 @@ class ControllerImpl {
183
181
  this._pendingHandlers = [];
184
182
  this._isConnected = true;
185
183
  this._isReady = true;
186
- if (initData.gameInProgress && this.transport) {
187
- this.logger.lifecycle("Game in progress detected, requesting state recovery");
188
- this.transport.emit(events.SMORE_EVENTS.STATE_GET_ALL, {});
189
- }
190
184
  for (const buffered of this._outboundBuffer) {
191
185
  try {
192
186
  switch (buffered.method) {
@@ -337,44 +331,6 @@ class ControllerImpl {
337
331
  this.logger.lifecycle("Connection restored");
338
332
  this._emitLifecycle("$connection-change", true);
339
333
  });
340
- this.registerHandler(events.SMORE_EVENTS.STATE_CHANGED, (raw) => {
341
- const data = raw;
342
- if (typeof data?.playerIndex === "number" && data.state) {
343
- this._customStates.set(data.playerIndex, data.state);
344
- this._stateChangeListeners.forEach((cb) => {
345
- try {
346
- cb(data.playerIndex, data.state);
347
- } catch (err) {
348
- this.handleError(
349
- new errors.SmoreSDKError("UNKNOWN", "Error in custom state change listener", {
350
- cause: err instanceof Error ? err : void 0
351
- })
352
- );
353
- }
354
- });
355
- }
356
- });
357
- this.registerHandler(events.SMORE_EVENTS.STATE_ALL, (raw) => {
358
- const data = raw;
359
- if (data?.states) {
360
- for (const [key, value] of Object.entries(data.states)) {
361
- const pi = Number(key);
362
- this._customStates.set(pi, value);
363
- this._stateChangeListeners.forEach((cb) => {
364
- try {
365
- cb(pi, value);
366
- } catch (err) {
367
- this.handleError(
368
- new errors.SmoreSDKError("UNKNOWN", "Error in custom state change listener", {
369
- cause: err instanceof Error ? err : void 0
370
- })
371
- );
372
- }
373
- });
374
- }
375
- this._emitLifecycle("$state-recovery", data.states);
376
- }
377
- });
378
334
  }
379
335
  /**
380
336
  * Sets up a user event handler for controller events.
@@ -478,26 +434,6 @@ class ControllerImpl {
478
434
  return this._addLifecycleListener("$game-over", callback);
479
435
  }
480
436
  // ---------------------------------------------------------------------------
481
- // Custom State Methods
482
- // ---------------------------------------------------------------------------
483
- setState(state) {
484
- const current = this._customStates.get(this._myPlayerIndex) ?? {};
485
- const merged = { ...current, ...state };
486
- this._customStates.set(this._myPlayerIndex, merged);
487
- if (this.transport) {
488
- this.transport.emit(events.SMORE_EVENTS.STATE_SET, { state });
489
- }
490
- }
491
- getMyState() {
492
- return this._customStates.get(this._myPlayerIndex);
493
- }
494
- onCustomStateChange(listener) {
495
- this._stateChangeListeners.add(listener);
496
- return () => {
497
- this._stateChangeListeners.delete(listener);
498
- };
499
- }
500
- // ---------------------------------------------------------------------------
501
437
  // Communication Methods
502
438
  // ---------------------------------------------------------------------------
503
439
  /**
@@ -710,8 +646,6 @@ class ControllerImpl {
710
646
  this.handlerToTransport.clear();
711
647
  this._pendingHandlers = [];
712
648
  this._lifecycleListeners.clear();
713
- this._customStates.clear();
714
- this._stateChangeListeners.clear();
715
649
  this._isConnected = false;
716
650
  this._outboundBuffer = [];
717
651
  if (this.transport) {