@phalanx-engine/server 0.1.2 → 0.1.3

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.
Files changed (2) hide show
  1. package/README.md +282 -214
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -2,13 +2,29 @@
2
2
 
3
3
  Server component of [Phalanx Engine](../README.md) - a game-agnostic deterministic lockstep multiplayer engine with authentication, matchmaking, and command synchronization.
4
4
 
5
- ## Features
6
-
7
- - **Deterministic Lockstep**: Synchronizes only player commands, game logic runs deterministically on each client
8
- - **Matchmaking**: Built-in support for various game modes (1v1, 2v2, 3v3, 4v4, FFA)
9
- - **Tick System**: Configurable tick rate with command batching
10
- - **Reconnection Support**: Players can rejoin matches after disconnection
11
- - **TypeScript**: Full TypeScript support with exported types
5
+ For a high-level overview of engine features, see the [root README](../README.md).
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [Requirements](#requirements)
11
+ - [Quick Start](#quick-start)
12
+ - [Minimal Configuration](#minimal-configuration)
13
+ - [Configuration Reference](#configuration-reference)
14
+ - [Game Types](#game-types)
15
+ - [Game Modes](#game-modes)
16
+ - [Event Hooks](#event-hooks)
17
+ - [API](#api)
18
+ - [Phalanx Class](#phalanx-class)
19
+ - [Socket.IO Protocol Reference](#socketio-protocol-reference)
20
+ - [Game Start Synchronization](#game-start-synchronization)
21
+ - [TLS / WSS Configuration](#tls--wss-configuration)
22
+ - [Advanced Topics](#advanced-topics)
23
+ - [HTTP Extension Hooks](#http-extension-hooks)
24
+ - [Private Room Recovery](#private-room-recovery)
25
+ - [Trusted Server-Side Private Room Creation](#trusted-server-side-private-room-creation)
26
+ - [Related Packages](#related-packages)
27
+ - [License](#license)
12
28
 
13
29
  ## Installation
14
30
 
@@ -16,6 +32,10 @@ Server component of [Phalanx Engine](../README.md) - a game-agnostic determinist
16
32
  npm install @phalanx-engine/server
17
33
  ```
18
34
 
35
+ ## Requirements
36
+
37
+ - Node.js `>= 24.0.0`
38
+
19
39
  ## Quick Start
20
40
 
21
41
  ```typescript
@@ -32,7 +52,32 @@ app.start().then(() => {
32
52
  });
33
53
  ```
34
54
 
35
- ## Configuration
55
+ ## Minimal Configuration
56
+
57
+ A typical production-ready configuration looks like this:
58
+
59
+ ```typescript
60
+ import { Phalanx } from '@phalanx-engine/server';
61
+
62
+ const app = new Phalanx({
63
+ port: 3000,
64
+ tickRate: 20,
65
+ gameMode: '3v3',
66
+ countdownSeconds: 5,
67
+ readyTimeoutMs: 30000,
68
+ enableStateHashing: true,
69
+ desync: {
70
+ enabled: true,
71
+ action: 'end-match',
72
+ },
73
+ });
74
+
75
+ await app.start();
76
+ ```
77
+
78
+ See the full reference below for every available option.
79
+
80
+ ## Configuration Reference
36
81
 
37
82
  ```typescript
38
83
  import { Phalanx, PhalanxConfig } from '@phalanx-engine/server';
@@ -130,207 +175,6 @@ const config: Partial<PhalanxConfig> = {
130
175
  const app = new Phalanx(config);
131
176
  ```
132
177
 
133
- ## Event Hooks
134
-
135
- ```typescript
136
- app.on('match-created', (match) => {
137
- console.log(`Match ${match.id} created with ${match.players.length} players`);
138
- });
139
-
140
- app.on('match-started', (match) => {
141
- console.log(`Match ${match.id} started!`);
142
- });
143
-
144
- app.on('player-command', (playerId, command) => {
145
- // Custom command validation
146
- return true; // or false to reject
147
- });
148
-
149
- app.on('player-timeout', (playerId, matchId) => {
150
- console.log(`Player ${playerId} timed out`);
151
- });
152
-
153
- app.on('player-disconnected', (playerId, matchId) => {
154
- console.log(`Player ${playerId} disconnected from match ${matchId}`);
155
- });
156
-
157
- app.on('player-reconnected', (playerId, matchId) => {
158
- console.log(`Player ${playerId} reconnected to match ${matchId}`);
159
- });
160
- ```
161
-
162
- ## HTTP Extension Hooks
163
-
164
- Use `extraRequestHandler` when an embedding server needs a small HTTP surface on the same port as Phalanx, such as a Telegram webhook, an internal health probe, or a signed partner callback.
165
-
166
- ```typescript
167
- import { Phalanx } from '@phalanx-engine/server';
168
-
169
- const app = new Phalanx({
170
- port: 3000,
171
- extraRequestHandler: async (req, res) => {
172
- if (req.method !== 'POST' || req.url !== '/telegram/webhook') {
173
- return false; // fall through to Phalanx built-in routes
174
- }
175
-
176
- if (req.headers['x-telegram-bot-api-secret-token'] !== process.env.WEBHOOK_SECRET) {
177
- res.writeHead(401);
178
- res.end();
179
- return true;
180
- }
181
-
182
- // Parse and handle the request body here.
183
- res.writeHead(200);
184
- res.end();
185
- return true;
186
- },
187
- });
188
- ```
189
-
190
- Request routing order is:
191
-
192
- 1. CORS preflight (`OPTIONS`) is answered by Phalanx.
193
- 2. `extraRequestHandler` is called.
194
- 3. Built-in routes run (`/`, `/health`, `/auth/token`).
195
- 4. Unknown routes receive `404`.
196
-
197
- Handler contract:
198
-
199
- - Return `true` only after fully handling the request, including writing headers/body and ending the response.
200
- - Return `false` to let Phalanx continue with its built-in routing.
201
- - Set any route-specific CORS or content headers yourself for handled custom routes.
202
- - Parse and size-limit request bodies yourself; Phalanx only parses bodies for its own built-in routes.
203
- - If the handler throws, Phalanx logs the error and sends a generic `500` response when headers have not already been sent.
204
-
205
- Treat custom HTTP routes as public internet-facing endpoints. Validate webhook secrets or signatures, avoid exposing trusted administrative operations to browsers, and apply rate limiting where appropriate.
206
-
207
- ## API
208
-
209
- ### Phalanx Class
210
-
211
- ```typescript
212
- class Phalanx {
213
- constructor(config?: Partial<PhalanxConfig>);
214
-
215
- // Lifecycle
216
- start(): Promise<void>;
217
- stop(): Promise<void>;
218
-
219
- // Trusted server-side integrations
220
- createPrivateRoomForHost(params: {
221
- playerId: string;
222
- username?: string;
223
- gameType?: string;
224
- }): RoomCreatedEvent;
225
-
226
- // Events
227
- on(event: PhalanxEvent, handler: Function): this;
228
- off(event: PhalanxEvent, handler: Function): this;
229
-
230
- // Runtime info
231
- getActiveMatches(): MatchInfo[];
232
- getQueueSize(): number;
233
- getConfig(): PhalanxConfig;
234
- }
235
- ```
236
-
237
- ### Client Events
238
-
239
- | Event | Emit | Receive | Description |
240
- | --------------------- | ---- | ------- | --------------------------------- |
241
- | `queue-join` | ✅ | | Join matchmaking queue |
242
- | `queue-leave` | ✅ | | Leave matchmaking queue |
243
- | `queue-status` | | ✅ | Queue join/leave confirmation |
244
- | `match-found` | | ✅ | Match created, countdown starting |
245
- | `match-waiting-for-players` | | ✅ | Match is waiting for disconnected participants before countdown |
246
- | `game-start` | | ✅ | Countdown finished, load assets |
247
- | `client-ready` | ✅ | | Client finished loading, ready for ticks |
248
- | `player-ready` | | ✅ | A player reported ready |
249
- | `match-end` | | ✅ | Match has ended |
250
- | `submit-commands` | ✅ | | Send game commands |
251
- | `submit-commands-ack` | | ✅ | Command acknowledgment |
252
- | `commands-batch` | | ✅ | All commands for a tick |
253
- | `tick-sync` | | ✅ | Periodic tick synchronization |
254
- | `countdown` | | ✅ | Countdown before game starts |
255
- | `submit-state-hash` | ✅ | | Submit state hash for desync detection |
256
- | `hash-comparison` | | ✅ | Server hash comparison result |
257
- | `pause-game` | ✅ | | Request game pause |
258
- | `resume-game` | ✅ | | Request game resume |
259
- | `game-paused` | | ✅ | Game was paused |
260
- | `game-resumed` | | ✅ | Game was resumed |
261
- | `reconnect-match` | ✅ | | Attempt to rejoin a match |
262
- | `reconnect-status` | | ✅ | Reconnection result |
263
- | `reconnect-state` | | ✅ | Game state for reconnection |
264
- | `player-disconnected` | | ✅ | Another player disconnected |
265
- | `player-reconnected` | | ✅ | Another player reconnected |
266
- | `room-create` | ✅ | | Create a private room |
267
- | `room-join` | ✅ | | Join a private room by invite code |
268
- | `room-cancel` | ✅ | | Cancel a previously created room |
269
- | `room-recover` | ✅ | | Reclaim a room after socket drop (see Private Room Recovery) |
270
- | `room-created` | | ✅ | Room created; contains invite `code` |
271
- | `room-error` | | ✅ | Room operation failed; contains `message` |
272
- | `room-expired` | | ✅ | Room TTL exceeded; server evicted the room |
273
- | `room-cancelled` | | ✅ | Host cancelled the room |
274
- | `room-recovered` | | ✅ | Room successfully reclaimed after socket drop |
275
-
276
- ### Game Start Synchronization
277
-
278
- After the countdown completes, the server emits `game-start` and enters a `waiting-for-ready` state. The tick loop does **not** start until all connected clients send `client-ready`. This prevents desync caused by clients with different asset loading times missing early ticks.
279
-
280
- If a client does not send `client-ready` within 30 seconds, the match ends with reason `'ready-timeout'`.
281
-
282
- ### Private Room Recovery
283
-
284
- Private rooms support a `room-recover` handshake so a participant whose socket was dropped (e.g. mobile OS suspended the WebSocket while the user was sharing the invite link) can seamlessly reclaim either a waiting room or a private-room match without losing the pending join or countdown state.
285
-
286
- `room-recover` requires both the deterministic `playerId` and the original room `code`. Treat the room code as a bearer recovery secret: possession of the code is what proves the caller is allowed to recover that waiting room or match.
287
-
288
- #### Recovery protocol
289
-
290
- 1. Client calls `room-recover` with `{ playerId, code }` after reconnecting.
291
- 2. If the code still maps to a waiting room, only the original host can recover it. The server rebinds the host socket and emits `room-recovered`.
292
- 3. If the code has already been consumed into a match, any participant in that match can recover with the original code. The server emits `room-recovered` and delegates to the match reconnect flow.
293
- 4. If the match is still waiting for disconnected participants, reconnecting everyone starts the countdown. If the match is already in progress, the normal reconnect state is delivered.
294
- 5. Server responds with `room-error: { message: 'Room expired' }` on terminal failure. The same generic message is used for missing, expired, wrong-player, and already-finished cases so callers cannot probe room ownership.
295
-
296
- #### Server-side room TTL
297
-
298
- Waiting rooms are kept alive for `ROOM_TTL_MS` (default 5 minutes) from creation. If the host disconnects, the room remains recoverable until that original TTL expires; if no host socket is connected at expiry time, there is no `room-expired` recipient to notify. Clients should mirror this TTL in their local persistence layer so they do not attempt `room-recover` for a room the server has certainly already evicted.
299
-
300
- The client-side [`RoomRecoveryController`](../phalanx-client/README.md#mobile-friendly-room-recovery) handles the full recovery lifecycle automatically when `roomRecovery.enabled: true` is passed to `PhalanxClient`.
301
-
302
- ### Trusted Server-Side Private Room Creation
303
-
304
- Trusted integrations can create a private room for a host that is not currently connected over Socket.IO. This is intended for server-side entry points such as bot commands: the bot creates a room, sends the invite link containing the room code, and the host later opens the game and recovers the room with the same deterministic `playerId`.
305
-
306
- ```typescript
307
- const room = app.createPrivateRoomForHost({
308
- playerId: `telegram:${telegramUserId}`,
309
- username: telegramUsername,
310
- gameType: 'chapayev',
311
- });
312
-
313
- const inviteUrl = `https://game.example.com/?room=${room.code}`;
314
- ```
315
-
316
- `createPrivateRoomForHost`:
317
-
318
- - Requires `playerId` and accepts optional `username` and `gameType`.
319
- - Defaults `username` to `playerId` and `gameType` to the default game type when omitted.
320
- - Returns `{ code }`. If the same host already has an active waiting room, the existing room code is reused rather than creating a duplicate room.
321
- - Throws if Phalanx is not running, if the host is already queued for matchmaking, or if the host is already in an active private-room match.
322
- - Does not emit `room-created`, because there is no host socket yet.
323
-
324
- Typical bot/webhook flow:
325
-
326
- 1. A trusted HTTP handler validates the webhook request.
327
- 2. The handler derives a stable server-trusted `playerId` from the external identity, such as `telegram:123456`.
328
- 3. The handler calls `createPrivateRoomForHost` and sends the invite URL/code back through the external channel.
329
- 4. The host opens the game and emits `room-recover` with the same `playerId` and code.
330
- 5. The guest opens the invite and emits `room-join` with the code.
331
-
332
- Do not expose `createPrivateRoomForHost` as an unauthenticated browser endpoint. Browser clients should continue using the Socket.IO `room-create` flow, while trusted server integrations should authenticate the external request before creating rooms.
333
-
334
178
  ## Game Types
335
179
 
336
180
  Phalanx supports running multiple game types on a single server instance. Each game type can override base config values like `tickMode`, `tickRate`, `gameMode`, `countdownSeconds`, and `turnTimeoutMs`.
@@ -401,7 +245,130 @@ import { GAME_MODES } from '@phalanx-engine/server';
401
245
  // 'FFA4' - 4 players, 4 teams (Free For All)
402
246
  ```
403
247
 
404
- ## TLS/WSS Configuration
248
+ ## Event Hooks
249
+
250
+ ```typescript
251
+ app.on('match-created', (match) => {
252
+ console.log(`Match ${match.id} created with ${match.players.length} players`);
253
+ });
254
+
255
+ app.on('match-started', (match) => {
256
+ console.log(`Match ${match.id} started!`);
257
+ });
258
+
259
+ app.on('player-command', (playerId, command) => {
260
+ // Custom command validation
261
+ return true; // or false to reject
262
+ });
263
+
264
+ app.on('player-timeout', (playerId, matchId) => {
265
+ console.log(`Player ${playerId} timed out`);
266
+ });
267
+
268
+ app.on('player-disconnected', (playerId, matchId) => {
269
+ console.log(`Player ${playerId} disconnected from match ${matchId}`);
270
+ });
271
+
272
+ app.on('player-reconnected', (playerId, matchId) => {
273
+ console.log(`Player ${playerId} reconnected to match ${matchId}`);
274
+ });
275
+ ```
276
+
277
+ ## API
278
+
279
+ ### Phalanx Class
280
+
281
+ ```typescript
282
+ class Phalanx {
283
+ constructor(config?: Partial<PhalanxConfig>);
284
+
285
+ // Lifecycle
286
+ start(): Promise<void>;
287
+ stop(): Promise<void>;
288
+
289
+ // Trusted server-side integrations
290
+ createPrivateRoomForHost(params: {
291
+ playerId: string;
292
+ username?: string;
293
+ gameType?: string;
294
+ }): RoomCreatedEvent;
295
+
296
+ // Events
297
+ on(event: PhalanxEvent, handler: Function): this;
298
+ off(event: PhalanxEvent, handler: Function): this;
299
+
300
+ // Runtime info
301
+ getActiveMatches(): MatchInfo[];
302
+ getQueueSize(): number;
303
+ getConfig(): PhalanxConfig;
304
+ }
305
+ ```
306
+
307
+ ### Socket.IO Protocol Reference
308
+
309
+ | Event | Emit | Receive | Description |
310
+ | --------------------------- | ---- | ------- | --------------------------------------------------------------- |
311
+ | `queue-join` | ✅ | | Join matchmaking queue |
312
+ | `queue-leave` | ✅ | | Leave matchmaking queue |
313
+ | `queue-status` | | ✅ | Queue join/leave confirmation |
314
+ | `match-found` | | ✅ | Match created, countdown starting |
315
+ | `match-waiting-for-players` | | ✅ | Match is waiting for disconnected participants before countdown |
316
+ | `game-start` | | ✅ | Countdown finished, load assets |
317
+ | `client-ready` | ✅ | | Client finished loading, ready for ticks |
318
+ | `player-ready` | | ✅ | A player reported ready |
319
+ | `match-end` | | ✅ | Match has ended |
320
+ | `submit-commands` | ✅ | | Send game commands |
321
+ | `submit-commands-ack` | | ✅ | Command acknowledgment |
322
+ | `commands-batch` | | ✅ | All commands for a tick |
323
+ | `tick-sync` | | ✅ | Periodic tick synchronization |
324
+ | `countdown` | | ✅ | Countdown before game starts |
325
+ | `submit-state-hash` | ✅ | | Submit state hash for desync detection |
326
+ | `hash-comparison` | | ✅ | Server hash comparison result |
327
+ | `pause-game` | ✅ | | Request game pause |
328
+ | `resume-game` | ✅ | | Request game resume |
329
+ | `game-paused` | | ✅ | Game was paused |
330
+ | `game-resumed` | | ✅ | Game was resumed |
331
+ | `reconnect-match` | ✅ | | Attempt to rejoin a match |
332
+ | `reconnect-status` | | ✅ | Reconnection result |
333
+ | `reconnect-state` | | ✅ | Game state for reconnection |
334
+ | `player-disconnected` | | ✅ | Another player disconnected |
335
+ | `player-reconnected` | | ✅ | Another player reconnected |
336
+ | `room-create` | ✅ | | Create a private room |
337
+ | `room-join` | ✅ | | Join a private room by invite code |
338
+ | `room-cancel` | ✅ | | Cancel a previously created room |
339
+ | `room-recover` | ✅ | | Reclaim a room after socket drop (see Private Room Recovery) |
340
+ | `room-created` | | ✅ | Room created; contains invite `code` |
341
+ | `room-error` | | ✅ | Room operation failed; contains `message` |
342
+ | `room-expired` | | ✅ | Room TTL exceeded; server evicted the room |
343
+ | `room-cancelled` | | ✅ | Host cancelled the room |
344
+ | `room-recovered` | | ✅ | Room successfully reclaimed after socket drop |
345
+
346
+ ## Game Start Synchronization
347
+
348
+ Phalanx uses a **ready handshake** protocol to ensure all clients are fully initialized before the simulation begins. This prevents desync caused by clients with different asset download speeds missing early ticks.
349
+
350
+ ### How it works
351
+
352
+ 1. Server emits `game-start` after the countdown completes and enters a `waiting-for-ready` state.
353
+ 2. Each client receives `game-start`, loads assets, sets up the game world, and initializes all systems.
354
+ 3. Each client calls `client.sendReady()` to emit `client-ready` to the server.
355
+ 4. Server receives `client-ready` from **all** connected players, then starts the tick loop.
356
+ 5. All clients are guaranteed to be subscribed to tick events before tick 0.
357
+
358
+ If any client fails to send `client-ready` within 30 seconds, the match ends with reason `'ready-timeout'`.
359
+
360
+ ### Usage
361
+
362
+ Clients **must** call `sendReady()` after initialization:
363
+
364
+ ```typescript
365
+ client.on('gameStart', async () => {
366
+ await game.initialize(); // Load assets, set up ECS, etc.
367
+ client.sendReady(); // Tell the server we're ready
368
+ });
369
+ ```
370
+
371
+ ## TLS / WSS Configuration
405
372
 
406
373
  Phalanx supports secure WebSocket connections (WSS) for production environments.
407
374
 
@@ -452,16 +419,19 @@ const app = new Phalanx({
452
419
  ### Let's Encrypt Setup
453
420
 
454
421
  1. Install certbot:
422
+
455
423
  ```bash
456
424
  sudo apt install certbot
457
425
  ```
458
426
 
459
427
  2. Obtain certificates:
428
+
460
429
  ```bash
461
430
  sudo certbot certonly --standalone -d game.example.com
462
431
  ```
463
432
 
464
433
  3. Configure Phalanx:
434
+
465
435
  ```typescript
466
436
  const app = new Phalanx({
467
437
  port: 443,
@@ -494,13 +464,111 @@ const client = await PhalanxClient.create({
494
464
  });
495
465
  ```
496
466
 
497
- ## Related Packages
467
+ ## Advanced Topics
498
468
 
499
- - [@phalanx-engine/client](../phalanx-client) - Client library for connecting to Phalanx servers
469
+ ### HTTP Extension Hooks
500
470
 
501
- ## Requirements
471
+ Use `extraRequestHandler` when an embedding server needs a small HTTP surface on the same port as Phalanx, such as a Telegram webhook, an internal health probe, or a signed partner callback.
472
+
473
+ ```typescript
474
+ import { Phalanx } from '@phalanx-engine/server';
475
+
476
+ const app = new Phalanx({
477
+ port: 3000,
478
+ extraRequestHandler: async (req, res) => {
479
+ if (req.method !== 'POST' || req.url !== '/telegram/webhook') {
480
+ return false; // fall through to Phalanx built-in routes
481
+ }
482
+
483
+ if (
484
+ req.headers['x-telegram-bot-api-secret-token'] !==
485
+ process.env.WEBHOOK_SECRET
486
+ ) {
487
+ res.writeHead(401);
488
+ res.end();
489
+ return true;
490
+ }
491
+
492
+ // Parse and handle the request body here.
493
+ res.writeHead(200);
494
+ res.end();
495
+ return true;
496
+ },
497
+ });
498
+ ```
499
+
500
+ Request routing order is:
501
+
502
+ 1. CORS preflight (`OPTIONS`) is answered by Phalanx.
503
+ 2. `extraRequestHandler` is called.
504
+ 3. Built-in routes run (`/`, `/health`, `/auth/token`).
505
+ 4. Unknown routes receive `404`.
502
506
 
503
- - Node.js 18+
507
+ Handler contract:
508
+
509
+ - Return `true` only after fully handling the request, including writing headers/body and ending the response.
510
+ - Return `false` to let Phalanx continue with its built-in routing.
511
+ - Set any route-specific CORS or content headers yourself for handled custom routes.
512
+ - Parse and size-limit request bodies yourself; Phalanx only parses bodies for its own built-in routes.
513
+ - If the handler throws, Phalanx logs the error and sends a generic `500` response when headers have not already been sent.
514
+
515
+ Treat custom HTTP routes as public internet-facing endpoints. Validate webhook secrets or signatures, avoid exposing trusted administrative operations to browsers, and apply rate limiting where appropriate.
516
+
517
+ ### Private Room Recovery
518
+
519
+ Private rooms support a `room-recover` handshake so a participant whose socket was dropped (e.g. mobile OS suspended the WebSocket while the user was sharing the invite link) can seamlessly reclaim either a waiting room or a private-room match without losing the pending join or countdown state.
520
+
521
+ `room-recover` requires both the deterministic `playerId` and the original room `code`. Treat the room code as a bearer recovery secret: possession of the code is what proves the caller is allowed to recover that waiting room or match.
522
+
523
+ #### Recovery protocol
524
+
525
+ 1. Client calls `room-recover` with `{ playerId, code }` after reconnecting.
526
+ 2. If the code still maps to a waiting room, only the original host can recover it. The server rebinds the host socket and emits `room-recovered`.
527
+ 3. If the code has already been consumed into a match, any participant in that match can recover with the original code. The server emits `room-recovered` and delegates to the match reconnect flow.
528
+ 4. If the match is still waiting for disconnected participants, reconnecting everyone starts the countdown. If the match is already in progress, the normal reconnect state is delivered.
529
+ 5. Server responds with `room-error: { message: 'Room expired' }` on terminal failure. The same generic message is used for missing, expired, wrong-player, and already-finished cases so callers cannot probe room ownership.
530
+
531
+ #### Server-side room TTL
532
+
533
+ Waiting rooms are kept alive for `ROOM_TTL_MS` (default 5 minutes) from creation. If the host disconnects, the room remains recoverable until that original TTL expires; if no host socket is connected at expiry time, there is no `room-expired` recipient to notify. Clients should mirror this TTL in their local persistence layer so they do not attempt `room-recover` for a room the server has certainly already evicted.
534
+
535
+ The client-side [`RoomRecoveryController`](../phalanx-client/README.md#mobile-friendly-room-recovery) handles the full recovery lifecycle automatically when `roomRecovery.enabled: true` is passed to `PhalanxClient`.
536
+
537
+ ### Trusted Server-Side Private Room Creation
538
+
539
+ Trusted integrations can create a private room for a host that is not currently connected over Socket.IO. This is intended for server-side entry points such as bot commands: the bot creates a room, sends the invite link containing the room code, and the host later opens the game and recovers the room with the same deterministic `playerId`.
540
+
541
+ ```typescript
542
+ const room = app.createPrivateRoomForHost({
543
+ playerId: `telegram:${telegramUserId}`,
544
+ username: telegramUsername,
545
+ gameType: 'chapayev',
546
+ });
547
+
548
+ const inviteUrl = `https://game.example.com/?room=${room.code}`;
549
+ ```
550
+
551
+ `createPrivateRoomForHost`:
552
+
553
+ - Requires `playerId` and accepts optional `username` and `gameType`.
554
+ - Defaults `username` to `playerId` and `gameType` to the default game type when omitted.
555
+ - Returns `{ code }`. If the same host already has an active waiting room, the existing room code is reused rather than creating a duplicate room.
556
+ - Throws if Phalanx is not running, if the host is already queued for matchmaking, or if the host is already in an active private-room match.
557
+ - Does not emit `room-created`, because there is no host socket yet.
558
+
559
+ Typical bot/webhook flow:
560
+
561
+ 1. A trusted HTTP handler validates the webhook request.
562
+ 2. The handler derives a stable server-trusted `playerId` from the external identity, such as `telegram:123456`.
563
+ 3. The handler calls `createPrivateRoomForHost` and sends the invite URL/code back through the external channel.
564
+ 4. The host opens the game and emits `room-recover` with the same `playerId` and code.
565
+ 5. The guest opens the invite and emits `room-join` with the code.
566
+
567
+ Do not expose `createPrivateRoomForHost` as an unauthenticated browser endpoint. Browser clients should continue using the Socket.IO `room-create` flow, while trusted server integrations should authenticate the external request before creating rooms.
568
+
569
+ ## Related Packages
570
+
571
+ - [@phalanx-engine/client](../phalanx-client) - Client library for connecting to Phalanx servers
504
572
 
505
573
  ## License
506
574
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phalanx-engine/server",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Phalanx Engine Server - A game-agnostic deterministic lockstep multiplayer server with authentication, matchmaking, and command synchronization",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "pure-rand": "^7.0.1",
45
45
  "socket.io": "^4.7.0",
46
- "@phalanx-engine/math": "0.1.2"
46
+ "@phalanx-engine/math": "0.1.3"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^20.0.0",