@antha/multiplayer-core 0.0.1

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.
@@ -0,0 +1,417 @@
1
+ import { type JsonCompatibleValue, type MaybePromise, type PartialWithUndefined } from '@augment-vir/common';
2
+ import { type FindPortOptions } from '@rest-vir/api';
3
+ import { type AnyDuration } from 'date-vir';
4
+ import { ListenTarget } from 'typed-event-target';
5
+ import { type ClientId, type RoomId } from '../multiplayer-id.js';
6
+ import { type RoomInput } from '../webrtc/webrtc-multiplayer-controller.js';
7
+ import { RoomRejectionError } from './errors.js';
8
+ import { type MultiplayerApiClient } from './multiplayer-client.js';
9
+ /**
10
+ * Connection state for {@link MultiplayerRoomController}.
11
+ *
12
+ * @category Internal
13
+ */
14
+ export declare enum MultiplayerConnectionState {
15
+ Connecting = "connecting",
16
+ Connected = "connected",
17
+ /** The connection has not been started or has been gracefully terminated. */
18
+ Disconnected = "disconnected"
19
+ }
20
+ /**
21
+ * API and room connection state for {@link MultiplayerRoomController}.
22
+ *
23
+ * @category Internal
24
+ */
25
+ export type ApiAndRoomConnectionState = {
26
+ api: MultiplayerConnectionState | Error;
27
+ room: MultiplayerConnectionState | Error;
28
+ };
29
+ /**
30
+ * Empty or totally disconnected state for {@link ApiAndRoomConnectionState}.
31
+ *
32
+ * @category Internal
33
+ */
34
+ export declare const emptyApiAndRoomConnectionState: Readonly<ApiAndRoomConnectionState>;
35
+ /**
36
+ * The generic room transport surface exposed by {@link MultiplayerRoomController}. This will be
37
+ * implemented differently by each multiplayer state sync paradigm. Meaning, a different
38
+ * implementation for p2p-lock-step syncing, a different implementation for authoritative server
39
+ * state syncing, etc.
40
+ *
41
+ * @category Internal
42
+ */
43
+ export type MultiplayerRoomConnection<Message extends JsonCompatibleValue> = {
44
+ /** The id assigned to the local room client. */
45
+ clientId: ClientId;
46
+ /** Whether the local client is currently the room host. */
47
+ isHost(): boolean;
48
+ /** Whether the local client has an active room connection. */
49
+ isConnected(): boolean;
50
+ /** Get the ids of clients with active peer connections. */
51
+ getConnectedClientIds(): ClientId[];
52
+ /** Get every known client id in the room, including the host when known. */
53
+ getAllClientIds(): ClientId[];
54
+ /** Send a message through the room connection. */
55
+ sendMessage(message: Readonly<Message>): void;
56
+ /** Send a message to one specific room client. */
57
+ sendToOnlyOneClient(clientId: ClientId, message: Readonly<Message>): void;
58
+ /** Terminate the room connection and release transport resources. */
59
+ destroy(): void;
60
+ };
61
+ /**
62
+ * Constructor parameters for {@link MultiplayerRoomController}.
63
+ *
64
+ * @category Internal
65
+ */
66
+ export type MultiplayerRoomControllerParams<Message extends JsonCompatibleValue> = {
67
+ /**
68
+ * A unique string id that represents your game so that your lobby server can serve multiple
69
+ * games at once. Your lobby server will need to know this game id ahead of time and match it to
70
+ * your frontend's origin.
71
+ *
72
+ * If this is left empty, make sure your lobby server (if you have any) handles that, and only
73
+ * handles one game at a time.
74
+ */
75
+ gameId: string;
76
+ } & PartialWithUndefined<{
77
+ /**
78
+ * This is fired when a WebRTC peer attempts to connect to the host client (this will only be
79
+ * fired if your client is the host). Return `true` to accept the connection. Return `false` to
80
+ * reject it.
81
+ *
82
+ * @default accept all connections
83
+ */
84
+ acceptConnection?: ((connectingClientId: ClientId, controller: MultiplayerRoomController<Message>) => MaybePromise<boolean>) | undefined;
85
+ }>;
86
+ /**
87
+ * Multiplayer mode parameters for {@link MultiplayerRoomController}.
88
+ *
89
+ * @category Internal
90
+ */
91
+ export type MultiplayerInitParams = {
92
+ /**
93
+ * The origin of the server running the multiplayer API.
94
+ *
95
+ * @example 'http://localhost:3000'
96
+ */
97
+ backendOrigin: string;
98
+ } & PartialWithUndefined<{
99
+ /**
100
+ * Set to `undefined` or `false` to disable port scanning. Set to `true` to enable port
101
+ * scanning. Set to an options object to configure port scanning.
102
+ *
103
+ * It is useful to enable this so that clients can find the port that your multiplayer server is
104
+ * running on in case it must change. Note that port scanning will not be active if your
105
+ * `backendOrigin` does not contain a port.
106
+ *
107
+ * @default undefined
108
+ */
109
+ portScanOptions: Omit<FindPortOptions, 'startOrigin'> | boolean;
110
+ /**
111
+ * How long to wait before fetching the list of rooms again.
112
+ *
113
+ * @default {seconds: 10}
114
+ */
115
+ roomUpdateInterval: AnyDuration;
116
+ /**
117
+ * Optional stun server URLs to help with routing WebRTC connections. This is entirely optional,
118
+ * but might help with clients attempting to establish connections to each other.
119
+ */
120
+ stunServerUrls: ReadonlyArray<string>;
121
+ /** If set, this will override the internal multiplayer API. */
122
+ multiplayerApiClient: Readonly<MultiplayerApiClient>;
123
+ }>;
124
+ declare const ControllerMessageEvent_base: (new (eventInitDict: {
125
+ bubbles?: boolean;
126
+ cancelable?: boolean;
127
+ composed?: boolean;
128
+ detail: any;
129
+ }) => import("typed-event-target").TypedCustomEvent<any, "controller-message">) & Pick<{
130
+ new (type: string, eventInitDict?: EventInit): Event;
131
+ prototype: Event;
132
+ readonly NONE: 0;
133
+ readonly CAPTURING_PHASE: 1;
134
+ readonly AT_TARGET: 2;
135
+ readonly BUBBLING_PHASE: 3;
136
+ }, "prototype" | "NONE" | "CAPTURING_PHASE" | "AT_TARGET" | "BUBBLING_PHASE"> & Pick<import("typed-event-target").TypedCustomEvent<any, "controller-message">, "type">;
137
+ /**
138
+ * This is fired when a room message is received.
139
+ *
140
+ * @category Events
141
+ */
142
+ export declare class ControllerMessageEvent<Message extends JsonCompatibleValue> extends ControllerMessageEvent_base {
143
+ readonly sourceClientId: ClientId;
144
+ detail: Message;
145
+ constructor(sourceClientId: ClientId, detail: Message);
146
+ }
147
+ declare const ControllerRoomListEvent_base: (new (eventInitDict: {
148
+ bubbles?: boolean;
149
+ cancelable?: boolean;
150
+ composed?: boolean;
151
+ detail: Readonly<Partial<Record<RoomId, {
152
+ roomId: RoomId;
153
+ roomName: string;
154
+ clientCount: number;
155
+ hasRoomPassword: boolean;
156
+ }>>>;
157
+ }) => import("typed-event-target").TypedCustomEvent<Readonly<Partial<Record<RoomId, {
158
+ roomId: RoomId;
159
+ roomName: string;
160
+ clientCount: number;
161
+ hasRoomPassword: boolean;
162
+ }>>>, "controller-room-list">) & Pick<{
163
+ new (type: string, eventInitDict?: EventInit): Event;
164
+ prototype: Event;
165
+ readonly NONE: 0;
166
+ readonly CAPTURING_PHASE: 1;
167
+ readonly AT_TARGET: 2;
168
+ readonly BUBBLING_PHASE: 3;
169
+ }, "prototype" | "NONE" | "CAPTURING_PHASE" | "AT_TARGET" | "BUBBLING_PHASE"> & Pick<import("typed-event-target").TypedCustomEvent<Readonly<Partial<Record<RoomId, {
170
+ roomId: RoomId;
171
+ roomName: string;
172
+ clientCount: number;
173
+ hasRoomPassword: boolean;
174
+ }>>>, "controller-room-list">, "type">;
175
+ /**
176
+ * This is called whenever the room list updates, even if there were no changes to the room list.
177
+ * Note that room list updates are paused while the controller is connected to an actual room.
178
+ *
179
+ * @category Events
180
+ */
181
+ export declare class ControllerRoomListEvent extends ControllerRoomListEvent_base {
182
+ }
183
+ declare const ControllerClientEvent_base: (new (eventInitDict: {
184
+ bubbles?: boolean;
185
+ cancelable?: boolean;
186
+ composed?: boolean;
187
+ detail: Readonly<((Required<Pick<{
188
+ newHost: ClientId;
189
+ newMember: ClientId;
190
+ lostHost: ClientId;
191
+ lostMember: ClientId;
192
+ }, "newHost">> & Partial<Record<"newMember" | "lostHost" | "lostMember", never>>) | (Required<Pick<{
193
+ newHost: ClientId;
194
+ newMember: ClientId;
195
+ lostHost: ClientId;
196
+ lostMember: ClientId;
197
+ }, "newMember">> & Partial<Record<"newHost" | "lostHost" | "lostMember", never>>) | (Required<Pick<{
198
+ newHost: ClientId;
199
+ newMember: ClientId;
200
+ lostHost: ClientId;
201
+ lostMember: ClientId;
202
+ }, "lostHost">> & Partial<Record<"newHost" | "newMember" | "lostMember", never>>) | (Required<Pick<{
203
+ newHost: ClientId;
204
+ newMember: ClientId;
205
+ lostHost: ClientId;
206
+ lostMember: ClientId;
207
+ }, "lostMember">> & Partial<Record<"newHost" | "newMember" | "lostHost", never>>)) & Omit<{
208
+ newHost: ClientId;
209
+ newMember: ClientId;
210
+ lostHost: ClientId;
211
+ lostMember: ClientId;
212
+ }, "newHost" | "newMember" | "lostHost" | "lostMember">>;
213
+ }) => import("typed-event-target").TypedCustomEvent<Readonly<((Required<Pick<{
214
+ newHost: ClientId;
215
+ newMember: ClientId;
216
+ lostHost: ClientId;
217
+ lostMember: ClientId;
218
+ }, "newHost">> & Partial<Record<"newMember" | "lostHost" | "lostMember", never>>) | (Required<Pick<{
219
+ newHost: ClientId;
220
+ newMember: ClientId;
221
+ lostHost: ClientId;
222
+ lostMember: ClientId;
223
+ }, "newMember">> & Partial<Record<"newHost" | "lostHost" | "lostMember", never>>) | (Required<Pick<{
224
+ newHost: ClientId;
225
+ newMember: ClientId;
226
+ lostHost: ClientId;
227
+ lostMember: ClientId;
228
+ }, "lostHost">> & Partial<Record<"newHost" | "newMember" | "lostMember", never>>) | (Required<Pick<{
229
+ newHost: ClientId;
230
+ newMember: ClientId;
231
+ lostHost: ClientId;
232
+ lostMember: ClientId;
233
+ }, "lostMember">> & Partial<Record<"newHost" | "newMember" | "lostHost", never>>)) & Omit<{
234
+ newHost: ClientId;
235
+ newMember: ClientId;
236
+ lostHost: ClientId;
237
+ lostMember: ClientId;
238
+ }, "newHost" | "newMember" | "lostHost" | "lostMember">>, "controller-client">) & Pick<{
239
+ new (type: string, eventInitDict?: EventInit): Event;
240
+ prototype: Event;
241
+ readonly NONE: 0;
242
+ readonly CAPTURING_PHASE: 1;
243
+ readonly AT_TARGET: 2;
244
+ readonly BUBBLING_PHASE: 3;
245
+ }, "prototype" | "NONE" | "CAPTURING_PHASE" | "AT_TARGET" | "BUBBLING_PHASE"> & Pick<import("typed-event-target").TypedCustomEvent<Readonly<((Required<Pick<{
246
+ newHost: ClientId;
247
+ newMember: ClientId;
248
+ lostHost: ClientId;
249
+ lostMember: ClientId;
250
+ }, "newHost">> & Partial<Record<"newMember" | "lostHost" | "lostMember", never>>) | (Required<Pick<{
251
+ newHost: ClientId;
252
+ newMember: ClientId;
253
+ lostHost: ClientId;
254
+ lostMember: ClientId;
255
+ }, "newMember">> & Partial<Record<"newHost" | "lostHost" | "lostMember", never>>) | (Required<Pick<{
256
+ newHost: ClientId;
257
+ newMember: ClientId;
258
+ lostHost: ClientId;
259
+ lostMember: ClientId;
260
+ }, "lostHost">> & Partial<Record<"newHost" | "newMember" | "lostMember", never>>) | (Required<Pick<{
261
+ newHost: ClientId;
262
+ newMember: ClientId;
263
+ lostHost: ClientId;
264
+ lostMember: ClientId;
265
+ }, "lostMember">> & Partial<Record<"newHost" | "newMember" | "lostHost", never>>)) & Omit<{
266
+ newHost: ClientId;
267
+ newMember: ClientId;
268
+ lostHost: ClientId;
269
+ lostMember: ClientId;
270
+ }, "newHost" | "newMember" | "lostHost" | "lostMember">>, "controller-client">, "type">;
271
+ /**
272
+ * This is fired in the following situations:
273
+ *
274
+ * - A new host for the room was selected
275
+ * - The room host was lost
276
+ * - A new room client was added (only fired on the host client)
277
+ * - A room client was lost (only fired on the host client)
278
+ *
279
+ * @category Events
280
+ */
281
+ export declare class ControllerClientEvent extends ControllerClientEvent_base {
282
+ }
283
+ declare const ControllerConnectionEvent_base: (new (eventInitDict: {
284
+ bubbles?: boolean;
285
+ cancelable?: boolean;
286
+ composed?: boolean;
287
+ detail: ApiAndRoomConnectionState;
288
+ }) => import("typed-event-target").TypedCustomEvent<ApiAndRoomConnectionState, "controller-connection">) & Pick<{
289
+ new (type: string, eventInitDict?: EventInit): Event;
290
+ prototype: Event;
291
+ readonly NONE: 0;
292
+ readonly CAPTURING_PHASE: 1;
293
+ readonly AT_TARGET: 2;
294
+ readonly BUBBLING_PHASE: 3;
295
+ }, "prototype" | "NONE" | "CAPTURING_PHASE" | "AT_TARGET" | "BUBBLING_PHASE"> & Pick<import("typed-event-target").TypedCustomEvent<ApiAndRoomConnectionState, "controller-connection">, "type">;
296
+ /**
297
+ * Fires when the controller's connection state is updated.
298
+ *
299
+ * @category Events
300
+ */
301
+ export declare class ControllerConnectionEvent extends ControllerConnectionEvent_base {
302
+ }
303
+ /**
304
+ * All events emitted by this controller.
305
+ *
306
+ * @category Internal
307
+ */
308
+ export type AllMultiplayerRoomControllerEvents<Message extends JsonCompatibleValue> = ControllerMessageEvent<Message> | ControllerRoomListEvent | ControllerClientEvent | ControllerConnectionEvent;
309
+ /**
310
+ * A generic multiplayer room controller. It manages API connectivity, room discovery, WebRTC
311
+ * signaling, and generic room messages. It does not impose a game-state synchronization strategy.
312
+ *
313
+ * @category Main
314
+ */
315
+ export declare class MultiplayerRoomController<Message extends JsonCompatibleValue = any> extends ListenTarget<AllMultiplayerRoomControllerEvents<Message>> {
316
+ protected readonly params: MultiplayerRoomControllerParams<Message>;
317
+ /** All events emitted by this controller. */
318
+ static readonly events: {
319
+ ControllerMessageEvent: typeof ControllerMessageEvent;
320
+ ControllerRoomListEvent: typeof ControllerRoomListEvent;
321
+ ControllerClientEvent: typeof ControllerClientEvent;
322
+ ControllerConnectionEvent: typeof ControllerConnectionEvent;
323
+ };
324
+ /** All events emitted by this controller. */
325
+ readonly events: {
326
+ ControllerMessageEvent: typeof ControllerMessageEvent;
327
+ ControllerRoomListEvent: typeof ControllerRoomListEvent;
328
+ ControllerClientEvent: typeof ControllerClientEvent;
329
+ ControllerConnectionEvent: typeof ControllerConnectionEvent;
330
+ };
331
+ static readonly knownErrors: {
332
+ RoomRejectionError: typeof RoomRejectionError;
333
+ };
334
+ readonly knownErrors: {
335
+ RoomRejectionError: typeof RoomRejectionError;
336
+ };
337
+ /**
338
+ * Set to `false` to disable room updates, even when still not connected to a room in
339
+ * multiplayer mode.
340
+ */
341
+ enableRoomUpdates: boolean;
342
+ /** Currently joined room id. If a room has not been joined yet, this will be empty. */
343
+ readonly roomId: RoomId | undefined;
344
+ /** The current connection state of the controller's connection to a backend API. */
345
+ readonly apiConnectionState: ApiAndRoomConnectionState['api'];
346
+ /** The current connection state of the controller's connection to a multiplayer room. */
347
+ readonly roomConnectionState: ApiAndRoomConnectionState['room'];
348
+ /** Current room connection. */
349
+ currentConnection: MultiplayerRoomConnection<Message> | undefined;
350
+ /**
351
+ * Rooms that have rejected the current player, so the player doesn't keep trying to connect to
352
+ * them.
353
+ */
354
+ protected rejectedRoomIds: Set<RoomId>;
355
+ /**
356
+ * The current multiplayer API client. This will be `undefined` before multiplayer starts or if
357
+ * playing in single player.
358
+ */
359
+ multiplayerApiClient: Readonly<MultiplayerApiClient> | undefined;
360
+ /**
361
+ * Used to keep track of the room update interval. This will be set when the controller is
362
+ * constructed in multiplayer mode or when a room is left. This will be cleared when a room is
363
+ * joined or if the controller is destroyed.
364
+ */
365
+ protected roomUpdateIntervalId: ReturnType<typeof globalThis.setInterval> | undefined;
366
+ /** This is populated when `.initMultiplayer` is called. */
367
+ protected multiplayerParams: Readonly<MultiplayerInitParams> | undefined;
368
+ /**
369
+ * Get the current client's WebRTC client id. This will return `undefined` if there is no
370
+ * current connection.
371
+ */
372
+ getClientId(): ClientId | undefined;
373
+ /**
374
+ * Get all connected client ids.
375
+ *
376
+ * - For host clients, this indicates how many member clients are connected to the host client,
377
+ * _not_ including the host itself.
378
+ * - For non-host clients, this only lists the local connection used to reach the host.
379
+ */
380
+ getConnectedClientIds(): ClientId[];
381
+ /**
382
+ * Get all room client ids.
383
+ *
384
+ * - For host clients, this indicates how many clients are connected to the room, including the
385
+ * host client itself.
386
+ * - For non-host clients, this includes the member client and the host client once connected.
387
+ */
388
+ getAllClientIds(): ClientId[];
389
+ constructor(params: MultiplayerRoomControllerParams<Message>);
390
+ /**
391
+ * Start multiplayer mode. This initializes
392
+ * {@link MultiplayerRoomController.multiplayerApiClient} and
393
+ * {@link MultiplayerRoomController.roomUpdateIntervalId}.
394
+ */
395
+ initMultiplayer(params: Readonly<MultiplayerInitParams>): Promise<void>;
396
+ /** Send a generic message to the current room. */
397
+ sendMessage(message: Readonly<Message>): void;
398
+ /** Detects if this controller is the room host or not. */
399
+ isHost(): boolean;
400
+ /** Detects if this controller is connected to a room or not. */
401
+ isConnected(): boolean;
402
+ /** Cleanup everything. */
403
+ destroy(): void;
404
+ /**
405
+ * Join or create a room.
406
+ *
407
+ * @throws `Error` if this controller is already connected to a room.
408
+ */
409
+ joinOrCreateRoom(room: Readonly<RoomInput>): Promise<void>;
410
+ /** Leave the current room or single player connection. */
411
+ leaveRoom(): void;
412
+ /** Set the current connection state and fire listeners. */
413
+ protected updateConnectionState(state: Partial<ApiAndRoomConnectionState>): void;
414
+ /** Starts polling the multiplayer server for room updates and fires listeners. */
415
+ protected startRoomInterval(): void;
416
+ }
417
+ export {};