@colyseus/core 0.17.0 → 0.17.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/package.json +3 -4
- package/src/Debug.ts +37 -0
- package/src/IPC.ts +124 -0
- package/src/Logger.ts +30 -0
- package/src/MatchMaker.ts +1119 -0
- package/src/Protocol.ts +160 -0
- package/src/Room.ts +1797 -0
- package/src/Server.ts +325 -0
- package/src/Stats.ts +107 -0
- package/src/Transport.ts +207 -0
- package/src/errors/RoomExceptions.ts +141 -0
- package/src/errors/SeatReservationError.ts +5 -0
- package/src/errors/ServerError.ts +17 -0
- package/src/index.ts +81 -0
- package/src/matchmaker/Lobby.ts +68 -0
- package/src/matchmaker/LocalDriver/LocalDriver.ts +92 -0
- package/src/matchmaker/LocalDriver/Query.ts +94 -0
- package/src/matchmaker/RegisteredHandler.ts +172 -0
- package/src/matchmaker/controller.ts +64 -0
- package/src/matchmaker/driver.ts +191 -0
- package/src/presence/LocalPresence.ts +331 -0
- package/src/presence/Presence.ts +263 -0
- package/src/rooms/LobbyRoom.ts +135 -0
- package/src/rooms/RankedQueueRoom.ts +425 -0
- package/src/rooms/RelayRoom.ts +90 -0
- package/src/router/default_routes.ts +58 -0
- package/src/router/index.ts +43 -0
- package/src/serializer/NoneSerializer.ts +16 -0
- package/src/serializer/SchemaSerializer.ts +194 -0
- package/src/serializer/SchemaSerializerDebug.ts +148 -0
- package/src/serializer/Serializer.ts +9 -0
- package/src/utils/DevMode.ts +133 -0
- package/src/utils/StandardSchema.ts +20 -0
- package/src/utils/Utils.ts +169 -0
- package/src/utils/nanoevents.ts +20 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { Room, type Client, matchMaker, type IRoomCache, debugMatchMaking, ServerError, ErrorCode, isDevMode, } from "@colyseus/core";
|
|
2
|
+
|
|
3
|
+
export interface RankedQueueOptions {
|
|
4
|
+
/**
|
|
5
|
+
* number of players on each match
|
|
6
|
+
*/
|
|
7
|
+
maxPlayers?: number;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* name of the room to create
|
|
11
|
+
*/
|
|
12
|
+
matchRoomName: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* after these cycles, create a match with a bot
|
|
16
|
+
*/
|
|
17
|
+
maxWaitingCycles?: number;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* after this time, try to fit this client with a not-so-compatible group
|
|
21
|
+
*/
|
|
22
|
+
maxWaitingCyclesForPriority?: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* If set, teams must have the same size to be matched together
|
|
26
|
+
*/
|
|
27
|
+
maxTeamSize?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* If `allowIncompleteGroups` is true, players inside an unmatched group (that
|
|
31
|
+
* did not reached `maxPlayers`, and `maxWaitingCycles` has been
|
|
32
|
+
* reached) will be matched together. Your room should fill the remaining
|
|
33
|
+
* spots with "bots" on this case.
|
|
34
|
+
*/
|
|
35
|
+
allowIncompleteGroups?: boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Comparison function for matching clients to groups
|
|
39
|
+
* Returns true if the client is compatible with the group
|
|
40
|
+
*/
|
|
41
|
+
compare?: (client: ClientQueueData, matchGroup: MatchGroup) => boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
*
|
|
45
|
+
* When onGroupReady is set, the "roomNameToCreate" option is ignored.
|
|
46
|
+
*/
|
|
47
|
+
onGroupReady?: (this: RankedQueueRoom, group: MatchGroup) => Promise<IRoomCache>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MatchGroup {
|
|
51
|
+
averageRank: number;
|
|
52
|
+
clients: Array<Client<{ userData: ClientQueueData }>>,
|
|
53
|
+
ready?: boolean;
|
|
54
|
+
confirmed?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface MatchTeam {
|
|
58
|
+
averageRank: number;
|
|
59
|
+
clients: Array<Client<{ userData: ClientQueueData }>>,
|
|
60
|
+
teamId: string | symbol;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ClientQueueData {
|
|
64
|
+
/**
|
|
65
|
+
* Rank of the client
|
|
66
|
+
*/
|
|
67
|
+
rank: number;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Timestamp of when the client entered the queue
|
|
71
|
+
*/
|
|
72
|
+
currentCycle?: number;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Optional: if matching with a team, the team ID
|
|
76
|
+
*/
|
|
77
|
+
teamId?: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Additional options passed by the client when joining the room
|
|
81
|
+
*/
|
|
82
|
+
options?: any;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Match group the client is currently in
|
|
86
|
+
*/
|
|
87
|
+
group?: MatchGroup;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether the client has confirmed the connection to the room
|
|
91
|
+
*/
|
|
92
|
+
confirmed?: boolean;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether the client should be prioritized in the queue
|
|
96
|
+
* (e.g. for players that are waiting for a long time)
|
|
97
|
+
*/
|
|
98
|
+
highPriority?: boolean;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The last number of clients in the queue sent to the client
|
|
102
|
+
*/
|
|
103
|
+
lastQueueClientCount?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const DEFAULT_TEAM = Symbol("$default_team");
|
|
107
|
+
const DEFAULT_COMPARE = (client: ClientQueueData, matchGroup: MatchGroup) => {
|
|
108
|
+
const diff = Math.abs(client.rank - matchGroup.averageRank);
|
|
109
|
+
const diffRatio = (diff / matchGroup.averageRank);
|
|
110
|
+
// If diff ratio is too high, create a new match group
|
|
111
|
+
return (diff < 10 || diffRatio <= 2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class RankedQueueRoom extends Room {
|
|
115
|
+
maxPlayers = 4;
|
|
116
|
+
maxTeamSize: number;
|
|
117
|
+
allowIncompleteGroups: boolean = false;
|
|
118
|
+
|
|
119
|
+
maxWaitingCycles = 15;
|
|
120
|
+
maxWaitingCyclesForPriority?: number = 10;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Evaluate groups for each client at interval
|
|
124
|
+
*/
|
|
125
|
+
cycleTickInterval = 1000;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Groups of players per iteration
|
|
129
|
+
*/
|
|
130
|
+
groups: MatchGroup[] = [];
|
|
131
|
+
highPriorityGroups: MatchGroup[] = [];
|
|
132
|
+
|
|
133
|
+
matchRoomName: string;
|
|
134
|
+
|
|
135
|
+
protected compare = DEFAULT_COMPARE;
|
|
136
|
+
protected onGroupReady = (group: MatchGroup) => matchMaker.createRoom(this.matchRoomName, {});
|
|
137
|
+
|
|
138
|
+
messages = {
|
|
139
|
+
confirm: (client: Client, _: unknown) => {
|
|
140
|
+
const queueData = client.userData;
|
|
141
|
+
|
|
142
|
+
if (queueData && queueData.group && typeof (queueData.group.confirmed) === "number") {
|
|
143
|
+
queueData.confirmed = true;
|
|
144
|
+
queueData.group.confirmed++;
|
|
145
|
+
client.leave();
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onCreate(options: RankedQueueOptions) {
|
|
151
|
+
if (typeof(options.maxWaitingCycles) === "number") {
|
|
152
|
+
this.maxWaitingCycles = options.maxWaitingCycles;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (typeof(options.maxPlayers) === "number") {
|
|
156
|
+
this.maxPlayers = options.maxPlayers;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof(options.maxTeamSize) === "number") {
|
|
160
|
+
this.maxTeamSize = options.maxTeamSize;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (typeof(options.allowIncompleteGroups) !== "undefined") {
|
|
164
|
+
this.allowIncompleteGroups = options.allowIncompleteGroups;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (typeof(options.compare) === "function") {
|
|
168
|
+
this.compare = options.compare;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (typeof(options.onGroupReady) === "function") {
|
|
172
|
+
this.onGroupReady = options.onGroupReady;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (options.matchRoomName) {
|
|
176
|
+
this.matchRoomName = options.matchRoomName;
|
|
177
|
+
|
|
178
|
+
} else {
|
|
179
|
+
throw new ServerError(ErrorCode.APPLICATION_ERROR, "RankedQueueRoom: 'matchRoomName' option is required.");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
debugMatchMaking("RankedQueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Redistribute clients into groups at every interval
|
|
186
|
+
*/
|
|
187
|
+
this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
onJoin(client: Client, options: any, auth?: unknown) {
|
|
191
|
+
this.addToQueue(client, {
|
|
192
|
+
rank: options.rank,
|
|
193
|
+
teamId: options.teamId,
|
|
194
|
+
options,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
addToQueue(client: Client, queueData: ClientQueueData) {
|
|
199
|
+
if (queueData.currentCycle === undefined) {
|
|
200
|
+
queueData.currentCycle = 0;
|
|
201
|
+
}
|
|
202
|
+
client.userData = queueData;
|
|
203
|
+
|
|
204
|
+
// FIXME: reassign groups upon joining [?] (without incrementing cycle count)
|
|
205
|
+
client.send("clients", 1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
createMatchGroup() {
|
|
209
|
+
const group: MatchGroup = { clients: [], averageRank: 0 };
|
|
210
|
+
this.groups.push(group);
|
|
211
|
+
return group;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
reassignMatchGroups() {
|
|
215
|
+
// Re-set all groups
|
|
216
|
+
this.groups.length = 0;
|
|
217
|
+
this.highPriorityGroups.length = 0;
|
|
218
|
+
|
|
219
|
+
const sortedClients = (this.clients)
|
|
220
|
+
.filter((client) => {
|
|
221
|
+
// Filter out:
|
|
222
|
+
// - clients that are not in the queue
|
|
223
|
+
// - clients that are already in a "ready" group
|
|
224
|
+
return (
|
|
225
|
+
client.userData &&
|
|
226
|
+
client.userData.group?.ready !== true
|
|
227
|
+
);
|
|
228
|
+
})
|
|
229
|
+
.sort((a, b) => {
|
|
230
|
+
//
|
|
231
|
+
// Sort by rank ascending
|
|
232
|
+
//
|
|
233
|
+
return a.userData.rank - b.userData.rank;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
//
|
|
237
|
+
// The room either distribute by teams or by clients
|
|
238
|
+
//
|
|
239
|
+
if (typeof(this.maxTeamSize) === "number") {
|
|
240
|
+
this.redistributeTeams(sortedClients);
|
|
241
|
+
|
|
242
|
+
} else {
|
|
243
|
+
this.redistributeClients(sortedClients);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.evaluateHighPriorityGroups();
|
|
247
|
+
this.processGroupsReady();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
redistributeTeams(sortedClients: Client<{ userData: ClientQueueData }>[]) {
|
|
251
|
+
const teamsByID: { [teamId: string | symbol]: MatchTeam } = {};
|
|
252
|
+
|
|
253
|
+
sortedClients.forEach((client) => {
|
|
254
|
+
const teamId = client.userData.teamId || DEFAULT_TEAM;
|
|
255
|
+
|
|
256
|
+
// Create a new team if it doesn't exist
|
|
257
|
+
if (!teamsByID[teamId]) {
|
|
258
|
+
teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
teamsByID[teamId].averageRank += client.userData.rank;
|
|
262
|
+
teamsByID[teamId].clients.push(client);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Calculate average rank for each team
|
|
266
|
+
let teams = Object.values(teamsByID).map((team) => {
|
|
267
|
+
team.averageRank /= team.clients.length;
|
|
268
|
+
return team;
|
|
269
|
+
}).sort((a, b) => {
|
|
270
|
+
// Sort by average rank ascending
|
|
271
|
+
return a.averageRank - b.averageRank;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Iterate over teams multiple times until all clients are assigned to a group
|
|
275
|
+
do {
|
|
276
|
+
let currentGroup: MatchGroup = this.createMatchGroup();
|
|
277
|
+
teams = teams.filter((team) => {
|
|
278
|
+
// Remove clients from the team and add them to the current group
|
|
279
|
+
const totalRank = team.averageRank * team.clients.length;
|
|
280
|
+
|
|
281
|
+
// currentGroup.averageRank = (currentGroup.averageRank === undefined)
|
|
282
|
+
// ? team.averageRank
|
|
283
|
+
// : (currentGroup.averageRank + team.averageRank) / ;
|
|
284
|
+
currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);
|
|
285
|
+
|
|
286
|
+
if (team.clients.length >= this.maxTeamSize) {
|
|
287
|
+
// team still has enough clients to form a group
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// increment cycle count for all clients in the team
|
|
292
|
+
team.clients.forEach((client) => client.userData.currentCycle++);
|
|
293
|
+
|
|
294
|
+
return false;
|
|
295
|
+
});
|
|
296
|
+
} while (teams.length >= 2);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
redistributeClients(
|
|
300
|
+
sortedClients: Client<{ userData: ClientQueueData }>[],
|
|
301
|
+
currentGroup: MatchGroup = this.createMatchGroup(),
|
|
302
|
+
totalRank: number = 0,
|
|
303
|
+
) {
|
|
304
|
+
for (let i = 0, l = sortedClients.length; i < l; i++) {
|
|
305
|
+
const client = sortedClients[i];
|
|
306
|
+
const userData = client.userData;
|
|
307
|
+
const currentCycle = userData.currentCycle++;
|
|
308
|
+
|
|
309
|
+
if (currentGroup.averageRank > 0) {
|
|
310
|
+
if (
|
|
311
|
+
!this.compare(userData, currentGroup) &&
|
|
312
|
+
!userData.highPriority
|
|
313
|
+
) {
|
|
314
|
+
currentGroup = this.createMatchGroup();
|
|
315
|
+
totalRank = 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
userData.group = currentGroup;
|
|
320
|
+
currentGroup.clients.push(client);
|
|
321
|
+
|
|
322
|
+
totalRank += userData.rank;
|
|
323
|
+
currentGroup.averageRank = totalRank / currentGroup.clients.length;
|
|
324
|
+
|
|
325
|
+
// Enough players in the group, mark it as ready!
|
|
326
|
+
if (currentGroup.clients.length === this.maxPlayers) {
|
|
327
|
+
currentGroup.ready = true;
|
|
328
|
+
currentGroup = this.createMatchGroup();
|
|
329
|
+
totalRank = 0;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {
|
|
334
|
+
/**
|
|
335
|
+
* Match long-waiting clients with bots
|
|
336
|
+
*/
|
|
337
|
+
if (this.highPriorityGroups.indexOf(currentGroup) === -1) {
|
|
338
|
+
this.highPriorityGroups.push(currentGroup);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
} else if (
|
|
342
|
+
this.maxWaitingCyclesForPriority !== undefined &&
|
|
343
|
+
currentCycle >= this.maxWaitingCyclesForPriority
|
|
344
|
+
) {
|
|
345
|
+
/**
|
|
346
|
+
* Force this client to join a group, even if rank is incompatible
|
|
347
|
+
*/
|
|
348
|
+
userData.highPriority = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return currentGroup;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
evaluateHighPriorityGroups() {
|
|
356
|
+
/**
|
|
357
|
+
* Evaluate groups with high priority clients
|
|
358
|
+
*/
|
|
359
|
+
this.highPriorityGroups.forEach((group) => {
|
|
360
|
+
group.ready = group.clients.every((c) => {
|
|
361
|
+
// Give new clients another chance to join a group that is not "high priority"
|
|
362
|
+
return c.userData?.currentCycle > 1;
|
|
363
|
+
// return c.userData?.currentCycle >= this.maxWaitingCycles;
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
processGroupsReady() {
|
|
369
|
+
this.groups.forEach(async (group) => {
|
|
370
|
+
if (group.ready) {
|
|
371
|
+
group.confirmed = 0;
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
/**
|
|
375
|
+
* Create room instance in the server.
|
|
376
|
+
*/
|
|
377
|
+
const room = await this.onGroupReady.call(this, group);
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Reserve a seat for each client in the group.
|
|
381
|
+
* (If one fails, force all clients to leave, re-queueing is up to the client-side logic)
|
|
382
|
+
*/
|
|
383
|
+
await matchMaker.reserveMultipleSeatsFor(
|
|
384
|
+
room,
|
|
385
|
+
group.clients.map((client) => ({
|
|
386
|
+
sessionId: client.sessionId,
|
|
387
|
+
options: client.userData.options,
|
|
388
|
+
auth: client.auth,
|
|
389
|
+
})),
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Send room data for new WebSocket connection!
|
|
394
|
+
*/
|
|
395
|
+
group.clients.forEach((client, i) => {
|
|
396
|
+
client.send("seat", matchMaker.buildSeatReservation(room, client.sessionId));
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
} catch (e: any) {
|
|
400
|
+
//
|
|
401
|
+
// If creating a room, or reserving a seat failed - fail all clients
|
|
402
|
+
// Whether the clients retry or not is up to the client-side logic
|
|
403
|
+
//
|
|
404
|
+
group.clients.forEach(client => client.leave(1011, e.message));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
} else {
|
|
408
|
+
/**
|
|
409
|
+
* Notify clients within the group on how many players are in the queue
|
|
410
|
+
*/
|
|
411
|
+
group.clients.forEach((client) => {
|
|
412
|
+
//
|
|
413
|
+
// avoid sending the same number of clients to the client if it hasn't changed
|
|
414
|
+
//
|
|
415
|
+
const queueClientCount = group.clients.length;
|
|
416
|
+
if (client.userData.lastQueueClientCount !== queueClientCount) {
|
|
417
|
+
client.userData.lastQueueClientCount = queueClientCount;
|
|
418
|
+
client.send("clients", queueClientCount);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { defineTypes, MapSchema, Schema } from '@colyseus/schema';
|
|
2
|
+
|
|
3
|
+
import { Room } from '../Room.ts';
|
|
4
|
+
import type { Client } from '../Transport.ts';
|
|
5
|
+
import { CloseCode } from '../Protocol.ts';
|
|
6
|
+
|
|
7
|
+
class Player extends Schema {
|
|
8
|
+
public connected: boolean;
|
|
9
|
+
public name: string;
|
|
10
|
+
public sessionId: string;
|
|
11
|
+
}
|
|
12
|
+
defineTypes(Player, {
|
|
13
|
+
connected: 'boolean',
|
|
14
|
+
name: 'string',
|
|
15
|
+
sessionId: 'string',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
class State extends Schema {
|
|
19
|
+
public players = new MapSchema<Player>();
|
|
20
|
+
}
|
|
21
|
+
defineTypes(State, {
|
|
22
|
+
players: { map: Player },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* client.joinOrCreate("relayroom", {
|
|
27
|
+
* maxClients: 10,
|
|
28
|
+
* allowReconnectionTime: 20
|
|
29
|
+
* });
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export class RelayRoom extends Room {
|
|
33
|
+
public state = new State();
|
|
34
|
+
public allowReconnectionTime: number = 0;
|
|
35
|
+
|
|
36
|
+
public onCreate(options: Partial<{
|
|
37
|
+
maxClients: number,
|
|
38
|
+
allowReconnectionTime: number,
|
|
39
|
+
metadata: any,
|
|
40
|
+
}>) {
|
|
41
|
+
if (options.maxClients) {
|
|
42
|
+
this.maxClients = options.maxClients;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (options.allowReconnectionTime) {
|
|
46
|
+
this.allowReconnectionTime = Math.min(options.allowReconnectionTime, 40);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (options.metadata) {
|
|
50
|
+
this.setMetadata(options.metadata);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.onMessage('*', (client: Client, type: string | number, message: any) => {
|
|
54
|
+
this.broadcast(type, [client.sessionId, message], { except: client });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public onJoin(client: Client, options: any = {}) {
|
|
59
|
+
const player = new Player();
|
|
60
|
+
|
|
61
|
+
player.connected = true;
|
|
62
|
+
player.sessionId = client.sessionId;
|
|
63
|
+
|
|
64
|
+
if (options.name) {
|
|
65
|
+
player.name = options.name;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.state.players.set(client.sessionId, player);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async onLeave(client: Client, code: number) {
|
|
72
|
+
if (this.allowReconnectionTime > 0) {
|
|
73
|
+
const player = this.state.players.get(client.sessionId);
|
|
74
|
+
player.connected = false;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
if (code === CloseCode.CONSENTED) {
|
|
78
|
+
throw new Error('consented leave');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await this.allowReconnection(client, this.allowReconnectionTime);
|
|
82
|
+
player.connected = true;
|
|
83
|
+
|
|
84
|
+
} catch (e) {
|
|
85
|
+
this.state.players.delete(client.sessionId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createEndpoint, createRouter } from "@colyseus/better-call";
|
|
2
|
+
import * as matchMaker from "../MatchMaker.ts";
|
|
3
|
+
import { getBearerToken } from "../utils/Utils.ts";
|
|
4
|
+
|
|
5
|
+
export const postMatchmakeMethod = createEndpoint("/matchmake/:method/:roomName", { method: "POST" }, async (ctx) => {
|
|
6
|
+
// do not accept matchmaking requests if already shutting down
|
|
7
|
+
if (matchMaker.state === matchMaker.MatchMakerState.SHUTTING_DOWN) {
|
|
8
|
+
throw ctx.error(503);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const requestHeaders = ctx.request.headers;
|
|
12
|
+
const headers = Object.assign(
|
|
13
|
+
{},
|
|
14
|
+
matchMaker.controller.DEFAULT_CORS_HEADERS,
|
|
15
|
+
matchMaker.controller.getCorsHeaders(requestHeaders)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const method = ctx.params.method;
|
|
19
|
+
const roomName = ctx.params.roomName;
|
|
20
|
+
|
|
21
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
22
|
+
ctx.setHeader(key, value);
|
|
23
|
+
})
|
|
24
|
+
ctx.setHeader('Content-Type', 'application/json');
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const clientOptions = ctx.body;
|
|
28
|
+
const response = await matchMaker.controller.invokeMethod(
|
|
29
|
+
method,
|
|
30
|
+
roomName,
|
|
31
|
+
clientOptions,
|
|
32
|
+
{
|
|
33
|
+
token: getBearerToken(ctx.request.headers.get('authorization')),
|
|
34
|
+
headers: ctx.request.headers,
|
|
35
|
+
ip: requestHeaders.get('x-forwarded-for') ?? requestHeaders.get('x-client-ip') ?? requestHeaders.get('x-real-ip'),
|
|
36
|
+
req: ctx.request as any,
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
//
|
|
41
|
+
// TODO: respond with protocol, if available
|
|
42
|
+
//
|
|
43
|
+
// // specify protocol, if available.
|
|
44
|
+
// if (this.transport.protocol !== undefined) {
|
|
45
|
+
// response.protocol = this.transport.protocol;
|
|
46
|
+
// }
|
|
47
|
+
|
|
48
|
+
return response;
|
|
49
|
+
|
|
50
|
+
} catch (e: any) {
|
|
51
|
+
throw ctx.error(e.code, { code: e.code, error: e.message, });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export function getDefaultRouter() {
|
|
57
|
+
return createRouter({ postMatchmakeMethod });
|
|
58
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Server } from "http";
|
|
2
|
+
import { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint } from "@colyseus/better-call";
|
|
3
|
+
import { toNodeHandler } from "@colyseus/better-call/node";
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
createEndpoint,
|
|
7
|
+
createMiddleware,
|
|
8
|
+
createInternalContext,
|
|
9
|
+
} from "@colyseus/better-call";
|
|
10
|
+
|
|
11
|
+
export { type Router, toNodeHandler };
|
|
12
|
+
|
|
13
|
+
export function bindRouterToServer(server: Server, router: Router) {
|
|
14
|
+
// check if the server is bound to an express app
|
|
15
|
+
const expressApp: any = server.listeners('request').find((listener: Function) =>
|
|
16
|
+
listener.name === "app" && listener['mountpath'] === '/');
|
|
17
|
+
|
|
18
|
+
if (expressApp) {
|
|
19
|
+
// bind the router to the express app
|
|
20
|
+
expressApp.use(toNodeHandler(router.handler));
|
|
21
|
+
|
|
22
|
+
} else {
|
|
23
|
+
// otherwise, bind the router to the http server
|
|
24
|
+
server.on('request', toNodeHandler(router.handler));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Do not use this directly. This is used internally by `@colyseus/playground`.
|
|
30
|
+
* TODO: refactor. Avoid using globals.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export let __globalEndpoints: Record<string, Endpoint>;
|
|
34
|
+
|
|
35
|
+
export function createRouter<
|
|
36
|
+
E extends Record<string, Endpoint>,
|
|
37
|
+
Config extends RouterConfig
|
|
38
|
+
>(endpoints: E, config?: Config) {
|
|
39
|
+
// TODO: refactor. Avoid using globals.
|
|
40
|
+
__globalEndpoints = endpoints;
|
|
41
|
+
|
|
42
|
+
return createBetterCallRouter({ ...endpoints, }, config);
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Client } from '../Transport.ts';
|
|
2
|
+
import type { Serializer } from './Serializer.ts';
|
|
3
|
+
|
|
4
|
+
export class NoneSerializer<T= any> implements Serializer<T> {
|
|
5
|
+
public id: string = 'none';
|
|
6
|
+
|
|
7
|
+
public reset(data: any) {}
|
|
8
|
+
|
|
9
|
+
public getFullState(client?: Client) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public applyPatches(clients: Client[], state: T): boolean {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|