@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.
- package/LICENSE-CC0 +121 -0
- package/LICENSE-MIT +21 -0
- package/README.md +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/multiplayer-api/errors.d.ts +13 -0
- package/dist/multiplayer-api/errors.js +15 -0
- package/dist/multiplayer-api/multiplayer-api.d.ts +215 -0
- package/dist/multiplayer-api/multiplayer-api.js +123 -0
- package/dist/multiplayer-api/multiplayer-client.d.ts +25 -0
- package/dist/multiplayer-api/multiplayer-client.js +18 -0
- package/dist/multiplayer-api/multiplayer-controller.d.ts +417 -0
- package/dist/multiplayer-api/multiplayer-controller.js +331 -0
- package/dist/multiplayer-api/multiplayer-socket-messages.d.ts +141 -0
- package/dist/multiplayer-api/multiplayer-socket-messages.js +114 -0
- package/dist/multiplayer-id.d.ts +59 -0
- package/dist/multiplayer-id.js +38 -0
- package/dist/room-handler-server/mock-room-handler-server-api-client.d.ts +47 -0
- package/dist/room-handler-server/mock-room-handler-server-api-client.js +107 -0
- package/dist/room-handler-server/multiplayer-room-handler.d.ts +121 -0
- package/dist/room-handler-server/multiplayer-room-handler.js +216 -0
- package/dist/webrtc/web-rtc-communication.d.ts +64 -0
- package/dist/webrtc/web-rtc-communication.js +54 -0
- package/dist/webrtc/webrtc-controller.d.ts +104 -0
- package/dist/webrtc/webrtc-controller.js +170 -0
- package/dist/webrtc/webrtc-multiplayer-controller.d.ts +263 -0
- package/dist/webrtc/webrtc-multiplayer-controller.js +397 -0
- package/package.json +62 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { waitUntil } from '@augment-vir/assert';
|
|
2
|
+
import { ensureError, makeWritable, } from '@augment-vir/common';
|
|
3
|
+
import { convertDuration } from 'date-vir';
|
|
4
|
+
import { defineTypedCustomEvent, ListenTarget } from 'typed-event-target';
|
|
5
|
+
import { WebrtcMultiplayerConnectionUpdateEvent, WebrtcMultiplayerController, WebrtcMultiplayerMessageEvent, } from '../webrtc/webrtc-multiplayer-controller.js';
|
|
6
|
+
import { RoomRejectionError } from './errors.js';
|
|
7
|
+
import { multiplayerHealthEndpoint, multiplayerRoomsEndpoint, } from './multiplayer-api.js';
|
|
8
|
+
import { createMultiplayerApiClient } from './multiplayer-client.js';
|
|
9
|
+
/**
|
|
10
|
+
* Connection state for {@link MultiplayerRoomController}.
|
|
11
|
+
*
|
|
12
|
+
* @category Internal
|
|
13
|
+
*/
|
|
14
|
+
export var MultiplayerConnectionState;
|
|
15
|
+
(function (MultiplayerConnectionState) {
|
|
16
|
+
MultiplayerConnectionState["Connecting"] = "connecting";
|
|
17
|
+
MultiplayerConnectionState["Connected"] = "connected";
|
|
18
|
+
/** The connection has not been started or has been gracefully terminated. */
|
|
19
|
+
MultiplayerConnectionState["Disconnected"] = "disconnected";
|
|
20
|
+
})(MultiplayerConnectionState || (MultiplayerConnectionState = {}));
|
|
21
|
+
/**
|
|
22
|
+
* Empty or totally disconnected state for {@link ApiAndRoomConnectionState}.
|
|
23
|
+
*
|
|
24
|
+
* @category Internal
|
|
25
|
+
*/
|
|
26
|
+
export const emptyApiAndRoomConnectionState = {
|
|
27
|
+
room: MultiplayerConnectionState.Disconnected,
|
|
28
|
+
api: MultiplayerConnectionState.Disconnected,
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* This is fired when a room message is received.
|
|
32
|
+
*
|
|
33
|
+
* @category Events
|
|
34
|
+
*/
|
|
35
|
+
export class ControllerMessageEvent extends defineTypedCustomEvent()('controller-message') {
|
|
36
|
+
sourceClientId;
|
|
37
|
+
constructor(sourceClientId, detail) {
|
|
38
|
+
super({
|
|
39
|
+
detail,
|
|
40
|
+
});
|
|
41
|
+
this.sourceClientId = sourceClientId;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* This is called whenever the room list updates, even if there were no changes to the room list.
|
|
46
|
+
* Note that room list updates are paused while the controller is connected to an actual room.
|
|
47
|
+
*
|
|
48
|
+
* @category Events
|
|
49
|
+
*/
|
|
50
|
+
export class ControllerRoomListEvent extends defineTypedCustomEvent()('controller-room-list') {
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* This is fired in the following situations:
|
|
54
|
+
*
|
|
55
|
+
* - A new host for the room was selected
|
|
56
|
+
* - The room host was lost
|
|
57
|
+
* - A new room client was added (only fired on the host client)
|
|
58
|
+
* - A room client was lost (only fired on the host client)
|
|
59
|
+
*
|
|
60
|
+
* @category Events
|
|
61
|
+
*/
|
|
62
|
+
export class ControllerClientEvent extends defineTypedCustomEvent()('controller-client') {
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Fires when the controller's connection state is updated.
|
|
66
|
+
*
|
|
67
|
+
* @category Events
|
|
68
|
+
*/
|
|
69
|
+
export class ControllerConnectionEvent extends defineTypedCustomEvent()('controller-connection') {
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A generic multiplayer room controller. It manages API connectivity, room discovery, WebRTC
|
|
73
|
+
* signaling, and generic room messages. It does not impose a game-state synchronization strategy.
|
|
74
|
+
*
|
|
75
|
+
* @category Main
|
|
76
|
+
*/
|
|
77
|
+
export class MultiplayerRoomController extends ListenTarget {
|
|
78
|
+
params;
|
|
79
|
+
/** All events emitted by this controller. */
|
|
80
|
+
static events = {
|
|
81
|
+
ControllerMessageEvent,
|
|
82
|
+
ControllerRoomListEvent,
|
|
83
|
+
ControllerClientEvent,
|
|
84
|
+
ControllerConnectionEvent,
|
|
85
|
+
};
|
|
86
|
+
/** All events emitted by this controller. */
|
|
87
|
+
events = MultiplayerRoomController.events;
|
|
88
|
+
static knownErrors = {
|
|
89
|
+
RoomRejectionError,
|
|
90
|
+
};
|
|
91
|
+
knownErrors = MultiplayerRoomController.knownErrors;
|
|
92
|
+
/**
|
|
93
|
+
* Set to `false` to disable room updates, even when still not connected to a room in
|
|
94
|
+
* multiplayer mode.
|
|
95
|
+
*/
|
|
96
|
+
enableRoomUpdates = true;
|
|
97
|
+
/** Currently joined room id. If a room has not been joined yet, this will be empty. */
|
|
98
|
+
roomId;
|
|
99
|
+
/** The current connection state of the controller's connection to a backend API. */
|
|
100
|
+
apiConnectionState = MultiplayerConnectionState.Disconnected;
|
|
101
|
+
/** The current connection state of the controller's connection to a multiplayer room. */
|
|
102
|
+
roomConnectionState = MultiplayerConnectionState.Disconnected;
|
|
103
|
+
/** Current room connection. */
|
|
104
|
+
currentConnection;
|
|
105
|
+
/**
|
|
106
|
+
* Rooms that have rejected the current player, so the player doesn't keep trying to connect to
|
|
107
|
+
* them.
|
|
108
|
+
*/
|
|
109
|
+
rejectedRoomIds = new Set();
|
|
110
|
+
/**
|
|
111
|
+
* The current multiplayer API client. This will be `undefined` before multiplayer starts or if
|
|
112
|
+
* playing in single player.
|
|
113
|
+
*/
|
|
114
|
+
multiplayerApiClient;
|
|
115
|
+
/**
|
|
116
|
+
* Used to keep track of the room update interval. This will be set when the controller is
|
|
117
|
+
* constructed in multiplayer mode or when a room is left. This will be cleared when a room is
|
|
118
|
+
* joined or if the controller is destroyed.
|
|
119
|
+
*/
|
|
120
|
+
roomUpdateIntervalId;
|
|
121
|
+
/** This is populated when `.initMultiplayer` is called. */
|
|
122
|
+
multiplayerParams;
|
|
123
|
+
/**
|
|
124
|
+
* Get the current client's WebRTC client id. This will return `undefined` if there is no
|
|
125
|
+
* current connection.
|
|
126
|
+
*/
|
|
127
|
+
getClientId() {
|
|
128
|
+
return this.currentConnection?.clientId;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get all connected client ids.
|
|
132
|
+
*
|
|
133
|
+
* - For host clients, this indicates how many member clients are connected to the host client,
|
|
134
|
+
* _not_ including the host itself.
|
|
135
|
+
* - For non-host clients, this only lists the local connection used to reach the host.
|
|
136
|
+
*/
|
|
137
|
+
getConnectedClientIds() {
|
|
138
|
+
return this.currentConnection?.getConnectedClientIds() || [];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get all room client ids.
|
|
142
|
+
*
|
|
143
|
+
* - For host clients, this indicates how many clients are connected to the room, including the
|
|
144
|
+
* host client itself.
|
|
145
|
+
* - For non-host clients, this includes the member client and the host client once connected.
|
|
146
|
+
*/
|
|
147
|
+
getAllClientIds() {
|
|
148
|
+
return this.currentConnection?.getAllClientIds() || [];
|
|
149
|
+
}
|
|
150
|
+
constructor(params) {
|
|
151
|
+
super();
|
|
152
|
+
this.params = params;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Start multiplayer mode. This initializes
|
|
156
|
+
* {@link MultiplayerRoomController.multiplayerApiClient} and
|
|
157
|
+
* {@link MultiplayerRoomController.roomUpdateIntervalId}.
|
|
158
|
+
*/
|
|
159
|
+
async initMultiplayer(params) {
|
|
160
|
+
if (this.currentConnection) {
|
|
161
|
+
throw new Error('Cannot start multiplayer mode again when a multiplayer connection already present.');
|
|
162
|
+
}
|
|
163
|
+
this.multiplayerParams = params;
|
|
164
|
+
this.updateConnectionState({
|
|
165
|
+
api: MultiplayerConnectionState.Connecting,
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
const api = params.multiplayerApiClient ||
|
|
169
|
+
(await createMultiplayerApiClient({
|
|
170
|
+
portScanOptions: params.portScanOptions,
|
|
171
|
+
backendOrigin: params.backendOrigin,
|
|
172
|
+
}));
|
|
173
|
+
const output = await api.fetch(multiplayerHealthEndpoint).GET();
|
|
174
|
+
if (!output.Ok) {
|
|
175
|
+
throw new Error(`Failed to find multiplayer API at ${api.baseUrl}.`);
|
|
176
|
+
}
|
|
177
|
+
this.multiplayerApiClient = api;
|
|
178
|
+
this.updateConnectionState({
|
|
179
|
+
api: MultiplayerConnectionState.Connected,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
this.updateConnectionState({
|
|
184
|
+
api: ensureError(error),
|
|
185
|
+
});
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
this.startRoomInterval();
|
|
189
|
+
}
|
|
190
|
+
/** Send a generic message to the current room. */
|
|
191
|
+
sendMessage(message) {
|
|
192
|
+
if (!this.currentConnection || !this.currentConnection.isConnected()) {
|
|
193
|
+
throw new Error('Cannot send message: not connected to a room.');
|
|
194
|
+
}
|
|
195
|
+
this.currentConnection.sendMessage(message);
|
|
196
|
+
}
|
|
197
|
+
/** Detects if this controller is the room host or not. */
|
|
198
|
+
isHost() {
|
|
199
|
+
return this.currentConnection?.isHost() || false;
|
|
200
|
+
}
|
|
201
|
+
/** Detects if this controller is connected to a room or not. */
|
|
202
|
+
isConnected() {
|
|
203
|
+
return this.currentConnection?.isConnected() || false;
|
|
204
|
+
}
|
|
205
|
+
/** Cleanup everything. */
|
|
206
|
+
destroy() {
|
|
207
|
+
super.destroy();
|
|
208
|
+
this.updateConnectionState({
|
|
209
|
+
room: MultiplayerConnectionState.Disconnected,
|
|
210
|
+
api: MultiplayerConnectionState.Disconnected,
|
|
211
|
+
});
|
|
212
|
+
this.currentConnection?.destroy();
|
|
213
|
+
globalThis.clearInterval(this.roomUpdateIntervalId);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Join or create a room.
|
|
217
|
+
*
|
|
218
|
+
* @throws `Error` if this controller is already connected to a room.
|
|
219
|
+
*/
|
|
220
|
+
async joinOrCreateRoom(room) {
|
|
221
|
+
if (this.currentConnection) {
|
|
222
|
+
throw new Error('Cannot join room: connection already established.');
|
|
223
|
+
}
|
|
224
|
+
else if (!this.multiplayerApiClient || !this.multiplayerParams) {
|
|
225
|
+
throw new Error('Cannot join room. Please start this controller in multiplayer mode to join rooms.');
|
|
226
|
+
}
|
|
227
|
+
else if (this.rejectedRoomIds.has(room.roomId)) {
|
|
228
|
+
throw new RoomRejectionError(room);
|
|
229
|
+
}
|
|
230
|
+
this.updateConnectionState({
|
|
231
|
+
room: MultiplayerConnectionState.Connecting,
|
|
232
|
+
});
|
|
233
|
+
const currentConnection = new WebrtcMultiplayerController(this.params.gameId, this.multiplayerApiClient, this.multiplayerParams.stunServerUrls || [], room, undefined, this.params.acceptConnection
|
|
234
|
+
? (data) => {
|
|
235
|
+
return this.params.acceptConnection?.(data.connectingClientId, this) ?? true;
|
|
236
|
+
}
|
|
237
|
+
: undefined);
|
|
238
|
+
this.currentConnection = currentConnection;
|
|
239
|
+
currentConnection.listen((WebrtcMultiplayerMessageEvent), (event) => {
|
|
240
|
+
this.dispatch(new ControllerMessageEvent(event.sourceClientId, event.detail));
|
|
241
|
+
});
|
|
242
|
+
currentConnection.listen(WebrtcMultiplayerConnectionUpdateEvent, (event) => {
|
|
243
|
+
this.dispatch(new ControllerClientEvent({
|
|
244
|
+
detail: event.detail,
|
|
245
|
+
}));
|
|
246
|
+
});
|
|
247
|
+
await currentConnection.initConnection();
|
|
248
|
+
const connectionResult = await waitUntil.isDefined(() => {
|
|
249
|
+
const connected = currentConnection.isConnected();
|
|
250
|
+
const destroyed = currentConnection.isDestroyed;
|
|
251
|
+
return !connected && !destroyed
|
|
252
|
+
? undefined
|
|
253
|
+
: {
|
|
254
|
+
connected,
|
|
255
|
+
destroyed,
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
if (connectionResult.connected) {
|
|
259
|
+
makeWritable(this).roomId = room.roomId;
|
|
260
|
+
globalThis.clearInterval(this.roomUpdateIntervalId);
|
|
261
|
+
this.updateConnectionState({
|
|
262
|
+
room: MultiplayerConnectionState.Connected,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
currentConnection.destroy();
|
|
267
|
+
this.rejectedRoomIds.add(room.roomId);
|
|
268
|
+
this.currentConnection = undefined;
|
|
269
|
+
const error = new RoomRejectionError(room);
|
|
270
|
+
this.updateConnectionState({
|
|
271
|
+
room: error,
|
|
272
|
+
});
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** Leave the current room or single player connection. */
|
|
277
|
+
leaveRoom() {
|
|
278
|
+
if (!this.currentConnection) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
makeWritable(this).roomId = undefined;
|
|
282
|
+
this.currentConnection.destroy();
|
|
283
|
+
this.currentConnection = undefined;
|
|
284
|
+
this.startRoomInterval();
|
|
285
|
+
this.updateConnectionState({
|
|
286
|
+
room: MultiplayerConnectionState.Disconnected,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/** Set the current connection state and fire listeners. */
|
|
290
|
+
updateConnectionState(state) {
|
|
291
|
+
if (state.api) {
|
|
292
|
+
makeWritable(this).apiConnectionState = state.api;
|
|
293
|
+
}
|
|
294
|
+
if (state.room) {
|
|
295
|
+
makeWritable(this).roomConnectionState = state.room;
|
|
296
|
+
}
|
|
297
|
+
this.dispatch(new ControllerConnectionEvent({
|
|
298
|
+
detail: {
|
|
299
|
+
room: this.roomConnectionState,
|
|
300
|
+
api: this.apiConnectionState,
|
|
301
|
+
},
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
/** Starts polling the multiplayer server for room updates and fires listeners. */
|
|
305
|
+
startRoomInterval() {
|
|
306
|
+
if (this.multiplayerApiClient) {
|
|
307
|
+
const roomUpdateMs = this.multiplayerParams?.roomUpdateInterval
|
|
308
|
+
? convertDuration(this.multiplayerParams.roomUpdateInterval, {
|
|
309
|
+
milliseconds: true,
|
|
310
|
+
}).milliseconds
|
|
311
|
+
: 10_000;
|
|
312
|
+
this.roomUpdateIntervalId = globalThis.setInterval(async () => {
|
|
313
|
+
if (this.currentConnection ||
|
|
314
|
+
!this.multiplayerApiClient ||
|
|
315
|
+
!this.enableRoomUpdates) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const output = await this.multiplayerApiClient.fetch(multiplayerRoomsEndpoint).GET({
|
|
319
|
+
searchParams: {
|
|
320
|
+
gameId: [this.params.gameId],
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
if (output.Ok) {
|
|
324
|
+
this.dispatch(new ControllerRoomListEvent({
|
|
325
|
+
detail: output.Ok.responseData,
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
}, roomUpdateMs);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { MultiplayerWebSocketMessageType } from '../webrtc/web-rtc-communication.js';
|
|
2
|
+
/**
|
|
3
|
+
* A shape definition for {@link ClientIdentification} when joining or creating a room.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export declare const clientIdShape: import("object-shape-tester").Shape<{
|
|
8
|
+
/** This id is used to keep track of each client on the multiplayer server. */
|
|
9
|
+
clientId: import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<import("../multiplayer-id.js").ClientId>>;
|
|
10
|
+
/**
|
|
11
|
+
* The id of the room that the user is communicating with. Set this either to to an existing
|
|
12
|
+
* room to join that room, or a new id to create a new room.
|
|
13
|
+
*/
|
|
14
|
+
roomId: import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<import("../multiplayer-id.js").RoomId>>;
|
|
15
|
+
/** The name of the room to create or join. */
|
|
16
|
+
roomName: string;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* Data included in each multiplayer server message that is used to identify the message client and
|
|
20
|
+
* the room they wish to join or host.
|
|
21
|
+
*
|
|
22
|
+
* @category Internal
|
|
23
|
+
*/
|
|
24
|
+
export type ClientIdentification = typeof clientIdShape.runtimeType;
|
|
25
|
+
/**
|
|
26
|
+
* Base shape definition for every message type.
|
|
27
|
+
*
|
|
28
|
+
* @category Internal
|
|
29
|
+
*/
|
|
30
|
+
export declare const baseMessageShape: import("object-shape-tester").Shape<{
|
|
31
|
+
/**
|
|
32
|
+
* The id of the original message. If a message is ever a response to another message, the
|
|
33
|
+
* message id will remain the same throughout the chain.
|
|
34
|
+
*/
|
|
35
|
+
messageId: import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<import("../multiplayer-id.js").SocketMessageId>>;
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* ==========================================================
|
|
39
|
+
*
|
|
40
|
+
* # Message Shape Definitions
|
|
41
|
+
*
|
|
42
|
+
* ==========================================================
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Shape definition for "answer" messages.
|
|
46
|
+
*
|
|
47
|
+
* @category Internal
|
|
48
|
+
*/
|
|
49
|
+
export declare const answerMessageShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
|
|
50
|
+
clientId: import("../multiplayer-id.js").ClientId;
|
|
51
|
+
roomId: import("../multiplayer-id.js").RoomId;
|
|
52
|
+
roomName: string;
|
|
53
|
+
} & {
|
|
54
|
+
messageId: import("../multiplayer-id.js").SocketMessageId;
|
|
55
|
+
} & {
|
|
56
|
+
type: MultiplayerWebSocketMessageType.Answer;
|
|
57
|
+
data: {
|
|
58
|
+
type: MultiplayerWebSocketMessageType.Answer;
|
|
59
|
+
sdp: string;
|
|
60
|
+
} | {
|
|
61
|
+
rejected: true;
|
|
62
|
+
};
|
|
63
|
+
}>>>;
|
|
64
|
+
/**
|
|
65
|
+
* Shape definition for "host ping" messages.
|
|
66
|
+
*
|
|
67
|
+
* @category Internal
|
|
68
|
+
*/
|
|
69
|
+
export declare const hostPingMessageShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
|
|
70
|
+
clientId: import("../multiplayer-id.js").ClientId;
|
|
71
|
+
roomId: import("../multiplayer-id.js").RoomId;
|
|
72
|
+
roomName: string;
|
|
73
|
+
} & {
|
|
74
|
+
messageId: import("../multiplayer-id.js").SocketMessageId;
|
|
75
|
+
} & {
|
|
76
|
+
type: MultiplayerWebSocketMessageType.HostPing;
|
|
77
|
+
clientSecret: string;
|
|
78
|
+
clientCount: number;
|
|
79
|
+
roomPassword: string;
|
|
80
|
+
}>>>;
|
|
81
|
+
/**
|
|
82
|
+
* Shape definition for "offer" messages forwarded from the multiplayer server to the host.
|
|
83
|
+
*
|
|
84
|
+
* @category Internal
|
|
85
|
+
*/
|
|
86
|
+
export declare const forwardedOfferMessageShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
|
|
87
|
+
clientId: import("../multiplayer-id.js").ClientId;
|
|
88
|
+
roomId: import("../multiplayer-id.js").RoomId;
|
|
89
|
+
roomName: string;
|
|
90
|
+
} & {
|
|
91
|
+
messageId: import("../multiplayer-id.js").SocketMessageId;
|
|
92
|
+
} & {
|
|
93
|
+
type: MultiplayerWebSocketMessageType.Offer;
|
|
94
|
+
data: {
|
|
95
|
+
type: MultiplayerWebSocketMessageType.Offer;
|
|
96
|
+
sdp: string;
|
|
97
|
+
};
|
|
98
|
+
}>>>;
|
|
99
|
+
/**
|
|
100
|
+
* Shape definition for "offer" messages.
|
|
101
|
+
*
|
|
102
|
+
* @category Internal
|
|
103
|
+
*/
|
|
104
|
+
export declare const offerMessageShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
|
|
105
|
+
messageId: import("../multiplayer-id.js").SocketMessageId;
|
|
106
|
+
} & {
|
|
107
|
+
clientId: import("../multiplayer-id.js").ClientId;
|
|
108
|
+
roomId: import("../multiplayer-id.js").RoomId;
|
|
109
|
+
roomName: string;
|
|
110
|
+
} & {
|
|
111
|
+
type: MultiplayerWebSocketMessageType.Offer;
|
|
112
|
+
data: {
|
|
113
|
+
type: MultiplayerWebSocketMessageType.Offer;
|
|
114
|
+
sdp: string;
|
|
115
|
+
};
|
|
116
|
+
} & {
|
|
117
|
+
clientSecret: string;
|
|
118
|
+
roomPassword: string;
|
|
119
|
+
}>>>;
|
|
120
|
+
/**
|
|
121
|
+
* Shape definition for "offer result" messages.
|
|
122
|
+
*
|
|
123
|
+
* @category Internal
|
|
124
|
+
*/
|
|
125
|
+
export declare const offerResultShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
|
|
126
|
+
messageId: import("../multiplayer-id.js").SocketMessageId;
|
|
127
|
+
} & {
|
|
128
|
+
type: MultiplayerWebSocketMessageType.OfferResult;
|
|
129
|
+
hostClientId: import("../multiplayer-id.js").ClientId;
|
|
130
|
+
}>>>;
|
|
131
|
+
/**
|
|
132
|
+
* Shape definition for error messages.
|
|
133
|
+
*
|
|
134
|
+
* @category Internal
|
|
135
|
+
*/
|
|
136
|
+
export declare const errorMessageShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
|
|
137
|
+
messageId: import("../multiplayer-id.js").SocketMessageId;
|
|
138
|
+
} & {
|
|
139
|
+
type: MultiplayerWebSocketMessageType.Error;
|
|
140
|
+
errorMessage: string;
|
|
141
|
+
}>>>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { defineShape, exactShape, intersectShape, unionShape } from 'object-shape-tester';
|
|
2
|
+
import { multiplayerIdShapes } from '../multiplayer-id.js';
|
|
3
|
+
import { MultiplayerWebSocketMessageType, webrtcAnswerShape, webrtcOfferShape, } from '../webrtc/web-rtc-communication.js';
|
|
4
|
+
/**
|
|
5
|
+
* A shape definition for {@link ClientIdentification} when joining or creating a room.
|
|
6
|
+
*
|
|
7
|
+
* @category Internal
|
|
8
|
+
*/
|
|
9
|
+
export const clientIdShape = defineShape({
|
|
10
|
+
/** This id is used to keep track of each client on the multiplayer server. */
|
|
11
|
+
clientId: multiplayerIdShapes.client(),
|
|
12
|
+
/**
|
|
13
|
+
* The id of the room that the user is communicating with. Set this either to to an existing
|
|
14
|
+
* room to join that room, or a new id to create a new room.
|
|
15
|
+
*/
|
|
16
|
+
roomId: multiplayerIdShapes.room(),
|
|
17
|
+
/** The name of the room to create or join. */
|
|
18
|
+
roomName: '',
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Base shape definition for every message type.
|
|
22
|
+
*
|
|
23
|
+
* @category Internal
|
|
24
|
+
*/
|
|
25
|
+
export const baseMessageShape = defineShape({
|
|
26
|
+
/**
|
|
27
|
+
* The id of the original message. If a message is ever a response to another message, the
|
|
28
|
+
* message id will remain the same throughout the chain.
|
|
29
|
+
*/
|
|
30
|
+
messageId: multiplayerIdShapes.socketMessage(),
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* ==========================================================
|
|
34
|
+
*
|
|
35
|
+
* # Message Shape Definitions
|
|
36
|
+
*
|
|
37
|
+
* ==========================================================
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Shape definition for "answer" messages.
|
|
41
|
+
*
|
|
42
|
+
* @category Internal
|
|
43
|
+
*/
|
|
44
|
+
export const answerMessageShape = defineShape(intersectShape(clientIdShape, baseMessageShape, {
|
|
45
|
+
type: exactShape(MultiplayerWebSocketMessageType.Answer),
|
|
46
|
+
/**
|
|
47
|
+
* This data object matches the `RTCSessionDescriptionInit` type from the TS lib. This data
|
|
48
|
+
* should be passed into `RTCPeerConnection.setRemoteDescription` to accept a WebRTC
|
|
49
|
+
* answer.
|
|
50
|
+
*/
|
|
51
|
+
data: unionShape({
|
|
52
|
+
rejected: exactShape(true),
|
|
53
|
+
}, webrtcAnswerShape),
|
|
54
|
+
}));
|
|
55
|
+
/**
|
|
56
|
+
* Shape definition for "host ping" messages.
|
|
57
|
+
*
|
|
58
|
+
* @category Internal
|
|
59
|
+
*/
|
|
60
|
+
export const hostPingMessageShape = defineShape(intersectShape(clientIdShape, baseMessageShape, {
|
|
61
|
+
type: exactShape(MultiplayerWebSocketMessageType.HostPing),
|
|
62
|
+
/** This secret is used to verify that the sender is indeed the host of the current room. */
|
|
63
|
+
clientSecret: '',
|
|
64
|
+
clientCount: -1,
|
|
65
|
+
roomPassword: '',
|
|
66
|
+
}));
|
|
67
|
+
/**
|
|
68
|
+
* Shape definition for "offer" messages forwarded from the multiplayer server to the host.
|
|
69
|
+
*
|
|
70
|
+
* @category Internal
|
|
71
|
+
*/
|
|
72
|
+
export const forwardedOfferMessageShape = defineShape(intersectShape(clientIdShape, baseMessageShape, {
|
|
73
|
+
type: exactShape(MultiplayerWebSocketMessageType.Offer),
|
|
74
|
+
/**
|
|
75
|
+
* This data object matches the `RTCSessionDescriptionInit` type from the TS lib. This data
|
|
76
|
+
* should be passed into `RTCPeerConnection.setRemoteDescription` when creating an offer.
|
|
77
|
+
*/
|
|
78
|
+
data: webrtcOfferShape,
|
|
79
|
+
}));
|
|
80
|
+
/**
|
|
81
|
+
* Shape definition for "offer" messages.
|
|
82
|
+
*
|
|
83
|
+
* @category Internal
|
|
84
|
+
*/
|
|
85
|
+
export const offerMessageShape = defineShape(intersectShape(forwardedOfferMessageShape, baseMessageShape, {
|
|
86
|
+
/**
|
|
87
|
+
* This secret is used to verify that a client is a host of a room. Do not share this with
|
|
88
|
+
* other clients.
|
|
89
|
+
*/
|
|
90
|
+
clientSecret: '',
|
|
91
|
+
/**
|
|
92
|
+
* Set this when joining a room with a password or when creating a room to set a room
|
|
93
|
+
* password.
|
|
94
|
+
*/
|
|
95
|
+
roomPassword: '',
|
|
96
|
+
}));
|
|
97
|
+
/**
|
|
98
|
+
* Shape definition for "offer result" messages.
|
|
99
|
+
*
|
|
100
|
+
* @category Internal
|
|
101
|
+
*/
|
|
102
|
+
export const offerResultShape = defineShape(intersectShape(baseMessageShape, {
|
|
103
|
+
type: exactShape(MultiplayerWebSocketMessageType.OfferResult),
|
|
104
|
+
hostClientId: multiplayerIdShapes.client(),
|
|
105
|
+
}));
|
|
106
|
+
/**
|
|
107
|
+
* Shape definition for error messages.
|
|
108
|
+
*
|
|
109
|
+
* @category Internal
|
|
110
|
+
*/
|
|
111
|
+
export const errorMessageShape = defineShape(intersectShape(baseMessageShape, {
|
|
112
|
+
type: exactShape(MultiplayerWebSocketMessageType.Error),
|
|
113
|
+
errorMessage: '',
|
|
114
|
+
}));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Branded } from '@augment-vir/common';
|
|
2
|
+
import { type TUnsafe } from '@sinclair/typebox';
|
|
3
|
+
import { type Shape } from 'object-shape-tester';
|
|
4
|
+
/**
|
|
5
|
+
* Unique id for a multiplayer room client.
|
|
6
|
+
*
|
|
7
|
+
* @category Internal
|
|
8
|
+
*/
|
|
9
|
+
export type ClientId = Branded<`${typeof idPrefixes.client}${string}`, 'multiplayer-client-id'>;
|
|
10
|
+
/**
|
|
11
|
+
* Unique id for a multiplayer room.
|
|
12
|
+
*
|
|
13
|
+
* @category Internal
|
|
14
|
+
*/
|
|
15
|
+
export type RoomId = Branded<`${typeof idPrefixes.room}${string}`, 'multiplayer-room-id'>;
|
|
16
|
+
/**
|
|
17
|
+
* Unique id for a multiplayer signaling socket message.
|
|
18
|
+
*
|
|
19
|
+
* @category Internal
|
|
20
|
+
*/
|
|
21
|
+
export type SocketMessageId = Branded<`${typeof idPrefixes.socketMessage}${string}`, 'multiplayer-socket-message-id'>;
|
|
22
|
+
/**
|
|
23
|
+
* String prefixes used to distinguish multiplayer id types.
|
|
24
|
+
*
|
|
25
|
+
* @category Internal
|
|
26
|
+
*/
|
|
27
|
+
export declare const idPrefixes: {
|
|
28
|
+
/** Prefix for {@link RoomId}. */
|
|
29
|
+
readonly room: "r_";
|
|
30
|
+
/** Prefix for {@link ClientId}. */
|
|
31
|
+
readonly client: "c_";
|
|
32
|
+
/** Prefix for {@link SocketMessageId}. */
|
|
33
|
+
readonly socketMessage: "sm_";
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Map of multiplayer id keys to their branded id types.
|
|
37
|
+
*
|
|
38
|
+
* @category Internal
|
|
39
|
+
*/
|
|
40
|
+
export type IdMap = {
|
|
41
|
+
/** Branded room id type. */
|
|
42
|
+
room: RoomId;
|
|
43
|
+
/** Branded client id type. */
|
|
44
|
+
client: ClientId;
|
|
45
|
+
/** Branded socket message id type. */
|
|
46
|
+
socketMessage: SocketMessageId;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Create branded multiplayer ids with the correct prefix for each id type.
|
|
50
|
+
*
|
|
51
|
+
* @category Internal
|
|
52
|
+
*/
|
|
53
|
+
export declare const createMultiplayerId: { [IdType in keyof typeof idPrefixes]: () => IdMap[IdType]; };
|
|
54
|
+
/**
|
|
55
|
+
* Runtime shapes for validating branded multiplayer ids.
|
|
56
|
+
*
|
|
57
|
+
* @category Internal
|
|
58
|
+
*/
|
|
59
|
+
export declare const multiplayerIdShapes: { [IdType in keyof typeof idPrefixes]: () => Shape<TUnsafe<IdMap[IdType]>>; };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { check } from '@augment-vir/assert';
|
|
2
|
+
import { createCuid2, mapObjectValues } from '@augment-vir/common';
|
|
3
|
+
import { createCustomShape } from 'object-shape-tester';
|
|
4
|
+
/**
|
|
5
|
+
* String prefixes used to distinguish multiplayer id types.
|
|
6
|
+
*
|
|
7
|
+
* @category Internal
|
|
8
|
+
*/
|
|
9
|
+
export const idPrefixes = {
|
|
10
|
+
/** Prefix for {@link RoomId}. */
|
|
11
|
+
room: 'r_',
|
|
12
|
+
/** Prefix for {@link ClientId}. */
|
|
13
|
+
client: 'c_',
|
|
14
|
+
/** Prefix for {@link SocketMessageId}. */
|
|
15
|
+
socketMessage: 'sm_',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Create branded multiplayer ids with the correct prefix for each id type.
|
|
19
|
+
*
|
|
20
|
+
* @category Internal
|
|
21
|
+
*/
|
|
22
|
+
export const createMultiplayerId = mapObjectValues(idPrefixes, (idKey, prefix) => {
|
|
23
|
+
return () => `${prefix}${createCuid2()}`;
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Runtime shapes for validating branded multiplayer ids.
|
|
27
|
+
*
|
|
28
|
+
* @category Internal
|
|
29
|
+
*/
|
|
30
|
+
export const multiplayerIdShapes = mapObjectValues(idPrefixes, (idKey) => {
|
|
31
|
+
return createCustomShape({
|
|
32
|
+
checkValue(value) {
|
|
33
|
+
return check.isString(value) && value.startsWith(idPrefixes[idKey]);
|
|
34
|
+
},
|
|
35
|
+
default: '',
|
|
36
|
+
name: `${idKey} multiplayer id shape`,
|
|
37
|
+
});
|
|
38
|
+
});
|