@colyseus/core 0.17.8 → 0.17.10
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/build/MatchMaker.cjs.map +2 -2
- package/build/MatchMaker.d.ts +2 -9
- package/build/MatchMaker.mjs.map +2 -2
- package/build/Room.cjs +1 -0
- package/build/Room.cjs.map +2 -2
- package/build/Room.d.ts +4 -16
- package/build/Room.mjs +1 -0
- package/build/Room.mjs.map +2 -2
- package/build/Server.cjs +1 -0
- package/build/Server.cjs.map +2 -2
- package/build/Server.d.ts +3 -3
- package/build/Server.mjs +1 -0
- package/build/Server.mjs.map +2 -2
- package/build/errors/RoomExceptions.cjs.map +2 -2
- package/build/errors/RoomExceptions.d.ts +2 -1
- package/build/errors/RoomExceptions.mjs.map +2 -2
- package/build/index.cjs +1 -0
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +3 -2
- package/build/index.mjs +2 -0
- package/build/index.mjs.map +2 -2
- package/build/rooms/RankedQueueRoom.cjs +21 -7
- package/build/rooms/RankedQueueRoom.cjs.map +2 -2
- package/build/rooms/RankedQueueRoom.d.ts +3 -1
- package/build/rooms/RankedQueueRoom.mjs +5 -1
- package/build/rooms/RankedQueueRoom.mjs.map +2 -2
- package/build/router/index.cjs.map +2 -2
- package/build/router/index.d.ts +2 -2
- package/build/router/index.mjs.map +2 -2
- package/package.json +2 -1
- package/src/MatchMaker.ts +3 -10
- package/src/Room.ts +11 -21
- package/src/Server.ts +3 -4
- package/src/errors/RoomExceptions.ts +2 -1
- package/src/index.ts +8 -1
- package/src/rooms/RankedQueueRoom.ts +7 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/errors/RoomExceptions.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ExtractMessageType
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["import type { ExtractMessageType } from '@colyseus/shared-types';\nimport type { Room } from '../Room.ts';\n\nexport type RoomMethodName = 'onCreate'\n | 'onAuth'\n | 'onJoin'\n | 'onLeave'\n | 'onDispose'\n | 'onMessage'\n | 'setSimulationInterval'\n | 'setInterval'\n | 'setTimeout';\n\nexport type RoomException<R extends Room = Room> =\n OnCreateException<R> |\n OnAuthException<R> |\n OnJoinException<R> |\n OnLeaveException<R> |\n OnDisposeException |\n OnMessageException<R> |\n SimulationIntervalException |\n TimedEventException;\n\nexport class OnCreateException<R extends Room = Room> extends Error {\n options: Parameters<R['onCreate']>[0];\n constructor(\n cause: Error,\n message: string,\n options: Parameters<R['onCreate']>[0],\n ) {\n super(message, { cause });\n this.name = 'OnCreateException';\n this.options = options;\n }\n}\n\nexport class OnAuthException<R extends Room = Room> extends Error {\n client: Parameters<R['onAuth']>[0];\n options: Parameters<R['onAuth']>[1];\n constructor(\n cause: Error,\n message: string,\n client: Parameters<R['onAuth']>[0],\n options: Parameters<R['onAuth']>[1],\n ) {\n super(message, { cause });\n this.name = 'OnAuthException';\n this.client = client;\n this.options = options;\n }\n}\n\nexport class OnJoinException<R extends Room = Room> extends Error {\n client: Parameters<R['onJoin']>[0];\n options: Parameters<R['onJoin']>[1];\n auth: Parameters<R['onJoin']>[2];\n constructor(\n cause: Error,\n message: string,\n client: Parameters<R['onJoin']>[0],\n options: Parameters<R['onJoin']>[1],\n auth: Parameters<R['onJoin']>[2],\n ) {\n super(message, { cause });\n this.name = 'OnJoinException';\n this.client = client;\n this.options = options;\n this.auth = auth;\n }\n}\n\nexport class OnLeaveException<R extends Room = Room> extends Error {\n client: Parameters<R['onLeave']>[0];\n consented: Parameters<R['onLeave']>[1];\n constructor(\n cause: Error,\n message: string,\n client: Parameters<R['onLeave']>[0],\n consented: Parameters<R['onLeave']>[1],\n ) {\n super(message, { cause });\n this.name = 'OnLeaveException';\n this.client = client;\n this.consented = consented;\n }\n}\n\nexport class OnDisposeException extends Error {\n constructor(\n cause: Error,\n message: string,\n ) {\n super(message, { cause });\n this.name = 'OnDisposeException';\n }\n}\n\nexport class OnMessageException<R extends Room, MessageType extends keyof R['messages'] = keyof R['messages']> extends Error {\n client: R['~client'];\n payload: ExtractMessageType<R['messages'][MessageType]>;\n type: MessageType;\n constructor(\n cause: Error,\n message: string,\n client: R['~client'],\n payload: ExtractMessageType<R['messages'][MessageType]>,\n type: MessageType,\n ) {\n super(message, { cause });\n this.name = 'OnMessageException';\n this.client = client;\n this.payload = payload;\n this.type = type;\n }\n\n public isType<T extends keyof R['messages']>(type: T): this is OnMessageException<R, T> {\n return (this.type as string) === type;\n }\n}\n\nexport class SimulationIntervalException extends Error {\n constructor(\n cause: Error,\n message: string,\n ) {\n super(message, { cause });\n this.name = 'SimulationIntervalException';\n }\n}\n\nexport class TimedEventException extends Error {\n public args: any[];\n constructor(\n cause: Error,\n message: string,\n ...args: any[]\n ) {\n super(message, { cause });\n this.name = 'TimedEventException';\n this.args = args;\n }\n}"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBO,IAAM,oBAAN,cAAuD,MAAM;AAAA,EAElE,YACE,OACA,SACA,SACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,UAAU;AAAA,EACjB;AACF;AAEO,IAAM,kBAAN,cAAqD,MAAM;AAAA,EAGhE,YACE,OACA,SACA,QACA,SACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,UAAU;AAAA,EACjB;AACF;AAEO,IAAM,kBAAN,cAAqD,MAAM;AAAA,EAIhE,YACE,OACA,SACA,QACA,SACA,MACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,UAAU;AACf,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,mBAAN,cAAsD,MAAM;AAAA,EAGjE,YACE,OACA,SACA,QACA,WACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,YAAY;AAAA,EACnB;AACF;AAEO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YACE,OACA,SACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,qBAAN,cAAgH,MAAM;AAAA,EAI3H,YACE,OACA,SACA,QACA,SACA,MACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,UAAU;AACf,SAAK,OAAO;AAAA,EACd;AAAA,EAEO,OAAsC,MAA2C;AACtF,WAAQ,KAAK,SAAoB;AAAA,EACnC;AACF;AAEO,IAAM,8BAAN,cAA0C,MAAM;AAAA,EACrD,YACE,OACA,SACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,sBAAN,cAAkC,MAAM;AAAA,EAE7C,YACE,OACA,YACG,MACH;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ExtractMessageType
|
|
1
|
+
import type { ExtractMessageType } from '@colyseus/shared-types';
|
|
2
|
+
import type { Room } from '../Room.ts';
|
|
2
3
|
export type RoomMethodName = 'onCreate' | 'onAuth' | 'onJoin' | 'onLeave' | 'onDispose' | 'onMessage' | 'setSimulationInterval' | 'setInterval' | 'setTimeout';
|
|
3
4
|
export type RoomException<R extends Room = Room> = OnCreateException<R> | OnAuthException<R> | OnJoinException<R> | OnLeaveException<R> | OnDisposeException | OnMessageException<R> | SimulationIntervalException | TimedEventException;
|
|
4
5
|
export declare class OnCreateException<R extends Room = Room> extends Error {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/errors/RoomExceptions.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ExtractMessageType
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["import type { ExtractMessageType } from '@colyseus/shared-types';\nimport type { Room } from '../Room.ts';\n\nexport type RoomMethodName = 'onCreate'\n | 'onAuth'\n | 'onJoin'\n | 'onLeave'\n | 'onDispose'\n | 'onMessage'\n | 'setSimulationInterval'\n | 'setInterval'\n | 'setTimeout';\n\nexport type RoomException<R extends Room = Room> =\n OnCreateException<R> |\n OnAuthException<R> |\n OnJoinException<R> |\n OnLeaveException<R> |\n OnDisposeException |\n OnMessageException<R> |\n SimulationIntervalException |\n TimedEventException;\n\nexport class OnCreateException<R extends Room = Room> extends Error {\n options: Parameters<R['onCreate']>[0];\n constructor(\n cause: Error,\n message: string,\n options: Parameters<R['onCreate']>[0],\n ) {\n super(message, { cause });\n this.name = 'OnCreateException';\n this.options = options;\n }\n}\n\nexport class OnAuthException<R extends Room = Room> extends Error {\n client: Parameters<R['onAuth']>[0];\n options: Parameters<R['onAuth']>[1];\n constructor(\n cause: Error,\n message: string,\n client: Parameters<R['onAuth']>[0],\n options: Parameters<R['onAuth']>[1],\n ) {\n super(message, { cause });\n this.name = 'OnAuthException';\n this.client = client;\n this.options = options;\n }\n}\n\nexport class OnJoinException<R extends Room = Room> extends Error {\n client: Parameters<R['onJoin']>[0];\n options: Parameters<R['onJoin']>[1];\n auth: Parameters<R['onJoin']>[2];\n constructor(\n cause: Error,\n message: string,\n client: Parameters<R['onJoin']>[0],\n options: Parameters<R['onJoin']>[1],\n auth: Parameters<R['onJoin']>[2],\n ) {\n super(message, { cause });\n this.name = 'OnJoinException';\n this.client = client;\n this.options = options;\n this.auth = auth;\n }\n}\n\nexport class OnLeaveException<R extends Room = Room> extends Error {\n client: Parameters<R['onLeave']>[0];\n consented: Parameters<R['onLeave']>[1];\n constructor(\n cause: Error,\n message: string,\n client: Parameters<R['onLeave']>[0],\n consented: Parameters<R['onLeave']>[1],\n ) {\n super(message, { cause });\n this.name = 'OnLeaveException';\n this.client = client;\n this.consented = consented;\n }\n}\n\nexport class OnDisposeException extends Error {\n constructor(\n cause: Error,\n message: string,\n ) {\n super(message, { cause });\n this.name = 'OnDisposeException';\n }\n}\n\nexport class OnMessageException<R extends Room, MessageType extends keyof R['messages'] = keyof R['messages']> extends Error {\n client: R['~client'];\n payload: ExtractMessageType<R['messages'][MessageType]>;\n type: MessageType;\n constructor(\n cause: Error,\n message: string,\n client: R['~client'],\n payload: ExtractMessageType<R['messages'][MessageType]>,\n type: MessageType,\n ) {\n super(message, { cause });\n this.name = 'OnMessageException';\n this.client = client;\n this.payload = payload;\n this.type = type;\n }\n\n public isType<T extends keyof R['messages']>(type: T): this is OnMessageException<R, T> {\n return (this.type as string) === type;\n }\n}\n\nexport class SimulationIntervalException extends Error {\n constructor(\n cause: Error,\n message: string,\n ) {\n super(message, { cause });\n this.name = 'SimulationIntervalException';\n }\n}\n\nexport class TimedEventException extends Error {\n public args: any[];\n constructor(\n cause: Error,\n message: string,\n ...args: any[]\n ) {\n super(message, { cause });\n this.name = 'TimedEventException';\n this.args = args;\n }\n}"],
|
|
5
|
+
"mappings": ";AAuBO,IAAM,oBAAN,cAAuD,MAAM;AAAA,EAElE,YACE,OACA,SACA,SACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,UAAU;AAAA,EACjB;AACF;AAEO,IAAM,kBAAN,cAAqD,MAAM;AAAA,EAGhE,YACE,OACA,SACA,QACA,SACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,UAAU;AAAA,EACjB;AACF;AAEO,IAAM,kBAAN,cAAqD,MAAM;AAAA,EAIhE,YACE,OACA,SACA,QACA,SACA,MACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,UAAU;AACf,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,mBAAN,cAAsD,MAAM;AAAA,EAGjE,YACE,OACA,SACA,QACA,WACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,YAAY;AAAA,EACnB;AACF;AAEO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YACE,OACA,SACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,qBAAN,cAAgH,MAAM;AAAA,EAI3H,YACE,OACA,SACA,QACA,SACA,MACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,UAAU;AACf,SAAK,OAAO;AAAA,EACd;AAAA,EAEO,OAAsC,MAA2C;AACtF,WAAQ,KAAK,SAAoB;AAAA,EACnC;AACF;AAEO,IAAM,8BAAN,cAA0C,MAAM;AAAA,EACrD,YACE,OACA,SACA;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,sBAAN,cAAkC,MAAM;AAAA,EAE7C,YACE,OACA,YACG,MACH;AACA,UAAM,SAAS,EAAE,MAAM,CAAC;AACxB,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/index.cjs
CHANGED
|
@@ -93,6 +93,7 @@ __export(index_exports, {
|
|
|
93
93
|
});
|
|
94
94
|
module.exports = __toCommonJS(index_exports);
|
|
95
95
|
var import_timer = require("@colyseus/timer");
|
|
96
|
+
var import_shared_types = require("@colyseus/shared-types");
|
|
96
97
|
var import_Server = require("./Server.cjs");
|
|
97
98
|
var import_Room = require("./Room.cjs");
|
|
98
99
|
var import_Protocol = require("./Protocol.cjs");
|
package/build/index.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Core classes\nexport { Server, defineRoom, defineServer, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, room, RoomInternalState, validate, type RoomOptions, type MessageHandlerWithFormat, type
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA,4BAAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAG7C,oBAAoF;AACpF,
|
|
4
|
+
"sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Shared types - re-export from @colyseus/shared-types for convenience\nexport {\n type InferState,\n type ExtractRoomMessages,\n type ExtractRoomClientMessages,\n} from '@colyseus/shared-types';\n\n// Core classes\nexport { Server, defineRoom, defineServer, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, room, RoomInternalState, validate, type RoomOptions, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';\nexport { Protocol, ErrorCode, getMessageBytes, CloseCode } from './Protocol.ts';\nexport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\nexport { ServerError } from './errors/ServerError.ts';\n\nexport {\n type RoomException,\n type RoomMethodName,\n OnCreateException,\n OnAuthException,\n OnJoinException,\n OnLeaveException,\n OnDisposeException,\n OnMessageException,\n SimulationIntervalException,\n TimedEventException,\n} from './errors/RoomExceptions.ts';\n\n// MatchMaker\nimport * as matchMaker from './MatchMaker.ts';\nexport { matchMaker };\nexport { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';\n\n// Driver\nexport * from './matchmaker/LocalDriver/LocalDriver.ts';\nexport { initializeRoomCache } from './matchmaker/driver.ts';\n\n// Transport\nexport { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, connectClientToRoom } from './Transport.ts';\n\n// Presence\nexport { type Presence } from './presence/Presence.ts';\nexport { LocalPresence } from './presence/LocalPresence.ts';\n\n// Serializers\nexport { type Serializer } from './serializer/Serializer.ts';\nexport { SchemaSerializer } from './serializer/SchemaSerializer.ts';\n// export { SchemaSerializerDebug } from './serializer/SchemaSerializerDebug.ts';\n\n// Utilities\nexport { Clock, Delayed };\nexport { generateId, Deferred, HttpServerMock, spliceOne, getBearerToken } from './utils/Utils.ts';\nexport { isDevMode } from './utils/DevMode.ts';\n\n// IPC\nexport { subscribeIPC, requestFromIPC } from './IPC.ts';\n\n// Debug\nexport {\n debugMatchMaking,\n debugMessage,\n debugPatch,\n debugError,\n debugConnection,\n debugDriver,\n debugPresence,\n debugAndPrintError,\n} from './Debug.ts';\n\n// Default rooms\nexport { LobbyRoom } from './rooms/LobbyRoom.ts';\nexport { RelayRoom } from './rooms/RelayRoom.ts';\nexport { RankedQueueRoom, type RankedQueueOptions, type MatchGroup, type MatchTeam, type ClientQueueData } from './rooms/RankedQueueRoom.ts';\n\n// Router / Endpoints\nexport {\n createEndpoint,\n createInternalContext,\n createMiddleware,\n createRouter,\n toNodeHandler,\n __globalEndpoints,\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n} from './router/index.ts';\n\n// Abstract logging support\nexport { logger } from './Logger.ts';\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA,4BAAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAG7C,0BAIO;AAGP,oBAAoF;AACpF,kBAAiM;AACjM,sBAAgE;AAChE,+BAAkC;AAClC,yBAA4B;AAE5B,4BAWO;AAGP,iBAA4B;AAE5B,mBAA4C;AAG5C,0BAAc,qDAnCd;AAoCA,oBAAoC;AAGpC,uBAA+I;AAG/I,sBAA8B;AAC9B,2BAA8B;AAG9B,wBAAgC;AAChC,8BAAiC;AAKjC,mBAAgF;AAChF,qBAA0B;AAG1B,iBAA6C;AAG7C,mBASO;AAGP,uBAA0B;AAC1B,uBAA0B;AAC1B,6BAAgH;AAGhH,oBAaO;AAGP,oBAAuB;",
|
|
6
6
|
"names": ["Clock"]
|
|
7
7
|
}
|
package/build/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ClockTimer as Clock, Delayed } from '@colyseus/timer';
|
|
2
|
+
export { type InferState, type ExtractRoomMessages, type ExtractRoomClientMessages, } from '@colyseus/shared-types';
|
|
2
3
|
export { Server, defineRoom, defineServer, type ServerOptions, type SDKTypes } from './Server.ts';
|
|
3
|
-
export { Room, room, RoomInternalState, validate, type RoomOptions, type MessageHandlerWithFormat, type
|
|
4
|
+
export { Room, room, RoomInternalState, validate, type RoomOptions, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';
|
|
4
5
|
export { Protocol, ErrorCode, getMessageBytes, CloseCode } from './Protocol.ts';
|
|
5
6
|
export { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';
|
|
6
7
|
export { ServerError } from './errors/ServerError.ts';
|
|
@@ -23,5 +24,5 @@ export { debugMatchMaking, debugMessage, debugPatch, debugError, debugConnection
|
|
|
23
24
|
export { LobbyRoom } from './rooms/LobbyRoom.ts';
|
|
24
25
|
export { RelayRoom } from './rooms/RelayRoom.ts';
|
|
25
26
|
export { RankedQueueRoom, type RankedQueueOptions, type MatchGroup, type MatchTeam, type ClientQueueData } from './rooms/RankedQueueRoom.ts';
|
|
26
|
-
export { createEndpoint, createInternalContext, createMiddleware, createRouter, toNodeHandler, __globalEndpoints, type Router, } from './router/index.ts';
|
|
27
|
+
export { createEndpoint, createInternalContext, createMiddleware, createRouter, toNodeHandler, __globalEndpoints, type Router, type RouterConfig, type Endpoint, type EndpointOptions, type EndpointContext, type StrictEndpoint, } from './router/index.ts';
|
|
27
28
|
export { logger } from './Logger.ts';
|
package/build/index.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// packages/core/src/index.ts
|
|
2
2
|
import { ClockTimer as Clock, Delayed } from "@colyseus/timer";
|
|
3
|
+
import {
|
|
4
|
+
} from "@colyseus/shared-types";
|
|
3
5
|
import { Server, defineRoom, defineServer } from "./Server.mjs";
|
|
4
6
|
import { Room, room, RoomInternalState, validate } from "./Room.mjs";
|
|
5
7
|
import { Protocol, ErrorCode, getMessageBytes, CloseCode } from "./Protocol.mjs";
|
package/build/index.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Core classes\nexport { Server, defineRoom, defineServer, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, room, RoomInternalState, validate, type RoomOptions, type MessageHandlerWithFormat, type
|
|
5
|
-
"mappings": ";AAAA,SAAS,cAAc,OAAO,eAAe;AAG7C,SAAS,QAAQ,YAAY,oBAAuD;AACpF,SAAS,MAAM,MAAM,mBAAmB,
|
|
4
|
+
"sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Shared types - re-export from @colyseus/shared-types for convenience\nexport {\n type InferState,\n type ExtractRoomMessages,\n type ExtractRoomClientMessages,\n} from '@colyseus/shared-types';\n\n// Core classes\nexport { Server, defineRoom, defineServer, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, room, RoomInternalState, validate, type RoomOptions, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';\nexport { Protocol, ErrorCode, getMessageBytes, CloseCode } from './Protocol.ts';\nexport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\nexport { ServerError } from './errors/ServerError.ts';\n\nexport {\n type RoomException,\n type RoomMethodName,\n OnCreateException,\n OnAuthException,\n OnJoinException,\n OnLeaveException,\n OnDisposeException,\n OnMessageException,\n SimulationIntervalException,\n TimedEventException,\n} from './errors/RoomExceptions.ts';\n\n// MatchMaker\nimport * as matchMaker from './MatchMaker.ts';\nexport { matchMaker };\nexport { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';\n\n// Driver\nexport * from './matchmaker/LocalDriver/LocalDriver.ts';\nexport { initializeRoomCache } from './matchmaker/driver.ts';\n\n// Transport\nexport { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, connectClientToRoom } from './Transport.ts';\n\n// Presence\nexport { type Presence } from './presence/Presence.ts';\nexport { LocalPresence } from './presence/LocalPresence.ts';\n\n// Serializers\nexport { type Serializer } from './serializer/Serializer.ts';\nexport { SchemaSerializer } from './serializer/SchemaSerializer.ts';\n// export { SchemaSerializerDebug } from './serializer/SchemaSerializerDebug.ts';\n\n// Utilities\nexport { Clock, Delayed };\nexport { generateId, Deferred, HttpServerMock, spliceOne, getBearerToken } from './utils/Utils.ts';\nexport { isDevMode } from './utils/DevMode.ts';\n\n// IPC\nexport { subscribeIPC, requestFromIPC } from './IPC.ts';\n\n// Debug\nexport {\n debugMatchMaking,\n debugMessage,\n debugPatch,\n debugError,\n debugConnection,\n debugDriver,\n debugPresence,\n debugAndPrintError,\n} from './Debug.ts';\n\n// Default rooms\nexport { LobbyRoom } from './rooms/LobbyRoom.ts';\nexport { RelayRoom } from './rooms/RelayRoom.ts';\nexport { RankedQueueRoom, type RankedQueueOptions, type MatchGroup, type MatchTeam, type ClientQueueData } from './rooms/RankedQueueRoom.ts';\n\n// Router / Endpoints\nexport {\n createEndpoint,\n createInternalContext,\n createMiddleware,\n createRouter,\n toNodeHandler,\n __globalEndpoints,\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n} from './router/index.ts';\n\n// Abstract logging support\nexport { logger } from './Logger.ts';\n"],
|
|
5
|
+
"mappings": ";AAAA,SAAS,cAAc,OAAO,eAAe;AAG7C;AAAA,OAIO;AAGP,SAAS,QAAQ,YAAY,oBAAuD;AACpF,SAAS,MAAM,MAAM,mBAAmB,gBAAyJ;AACjM,SAAS,UAAU,WAAW,iBAAiB,iBAAiB;AAChE,SAAS,yBAAyB;AAClC,SAAS,mBAAmB;AAE5B;AAAA,EAGE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,YAAY,gBAAgB;AAE5B,SAAS,aAAa,sBAAsB;AAG5C,cAAc;AACd,SAAS,2BAA2B;AAGpC,SAA4D,aAAa,aAAa,WAA8B,2BAA2B;AAG/I,eAA8B;AAC9B,SAAS,qBAAqB;AAG9B,eAAgC;AAChC,SAAS,wBAAwB;AAKjC,SAAS,YAAY,UAAU,gBAAgB,WAAW,sBAAsB;AAChF,SAAS,iBAAiB;AAG1B,SAAS,cAAc,sBAAsB;AAG7C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAC1B,SAAS,uBAAuG;AAGhH;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAOK;AAGP,SAAS,cAAc;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// packages/core/src/rooms/RankedQueueRoom.ts
|
|
@@ -23,14 +33,18 @@ __export(RankedQueueRoom_exports, {
|
|
|
23
33
|
RankedQueueRoom: () => RankedQueueRoom
|
|
24
34
|
});
|
|
25
35
|
module.exports = __toCommonJS(RankedQueueRoom_exports);
|
|
26
|
-
var
|
|
36
|
+
var import_Room = require("../Room.cjs");
|
|
37
|
+
var matchMaker = __toESM(require("../MatchMaker.cjs"), 1);
|
|
38
|
+
var import_Debug = require("../Debug.cjs");
|
|
39
|
+
var import_ServerError = require("../errors/ServerError.cjs");
|
|
40
|
+
var import_Protocol = require("../Protocol.cjs");
|
|
27
41
|
var DEFAULT_TEAM = /* @__PURE__ */ Symbol("$default_team");
|
|
28
42
|
var DEFAULT_COMPARE = (client, matchGroup) => {
|
|
29
43
|
const diff = Math.abs(client.rank - matchGroup.averageRank);
|
|
30
44
|
const diffRatio = diff / matchGroup.averageRank;
|
|
31
45
|
return diff < 10 || diffRatio <= 2;
|
|
32
46
|
};
|
|
33
|
-
var RankedQueueRoom = class extends
|
|
47
|
+
var RankedQueueRoom = class extends import_Room.Room {
|
|
34
48
|
constructor() {
|
|
35
49
|
super(...arguments);
|
|
36
50
|
this.maxPlayers = 4;
|
|
@@ -47,7 +61,7 @@ var RankedQueueRoom = class extends import_core.Room {
|
|
|
47
61
|
this.groups = [];
|
|
48
62
|
this.highPriorityGroups = [];
|
|
49
63
|
this.compare = DEFAULT_COMPARE;
|
|
50
|
-
this.onGroupReady = (group) =>
|
|
64
|
+
this.onGroupReady = (group) => matchMaker.createRoom(this.matchRoomName, {});
|
|
51
65
|
this.messages = {
|
|
52
66
|
confirm: (client, _) => {
|
|
53
67
|
const queueData = client.userData;
|
|
@@ -81,9 +95,9 @@ var RankedQueueRoom = class extends import_core.Room {
|
|
|
81
95
|
if (options.matchRoomName) {
|
|
82
96
|
this.matchRoomName = options.matchRoomName;
|
|
83
97
|
} else {
|
|
84
|
-
throw new
|
|
98
|
+
throw new import_ServerError.ServerError(import_Protocol.ErrorCode.APPLICATION_ERROR, "RankedQueueRoom: 'matchRoomName' option is required.");
|
|
85
99
|
}
|
|
86
|
-
(0,
|
|
100
|
+
(0, import_Debug.debugMatchMaking)("RankedQueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);
|
|
87
101
|
this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);
|
|
88
102
|
}
|
|
89
103
|
onJoin(client, options, auth) {
|
|
@@ -194,7 +208,7 @@ var RankedQueueRoom = class extends import_core.Room {
|
|
|
194
208
|
group.confirmed = 0;
|
|
195
209
|
try {
|
|
196
210
|
const room = await this.onGroupReady.call(this, group);
|
|
197
|
-
await
|
|
211
|
+
await matchMaker.reserveMultipleSeatsFor(
|
|
198
212
|
room,
|
|
199
213
|
group.clients.map((client) => ({
|
|
200
214
|
sessionId: client.sessionId,
|
|
@@ -203,7 +217,7 @@ var RankedQueueRoom = class extends import_core.Room {
|
|
|
203
217
|
}))
|
|
204
218
|
);
|
|
205
219
|
group.clients.forEach((client, i) => {
|
|
206
|
-
client.send("seat",
|
|
220
|
+
client.send("seat", matchMaker.buildSeatReservation(room, client.sessionId));
|
|
207
221
|
});
|
|
208
222
|
} catch (e) {
|
|
209
223
|
group.clients.forEach((client) => client.leave(1011, e.message));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/rooms/RankedQueueRoom.ts"],
|
|
4
|
-
"sourcesContent": ["import { Room, type Client, matchMaker, type IRoomCache, debugMatchMaking, ServerError, ErrorCode } from \"@colyseus/core\";\n\nexport interface RankedQueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: ClientQueueData, matchGroup: MatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: RankedQueueRoom, group: MatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface MatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: ClientQueueData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface MatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: ClientQueueData }>>,\n teamId: string | symbol;\n}\n\nexport interface ClientQueueData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: MatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: ClientQueueData, matchGroup: MatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class RankedQueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: MatchGroup[] = [];\n highPriorityGroups: MatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: MatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave();\n }\n },\n }\n\n onCreate(options: RankedQueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"RankedQueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"RankedQueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: Client, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: Client, queueData: ClientQueueData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: MatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: ClientQueueData }>[]) {\n const teamsByID: { [teamId: string | symbol]: MatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: MatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: ClientQueueData }>[],\n currentGroup: MatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { ErrorCode } from '../Protocol.ts';\n\nexport interface RankedQueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: ClientQueueData, matchGroup: MatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: RankedQueueRoom, group: MatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface MatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: ClientQueueData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface MatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: ClientQueueData }>>,\n teamId: string | symbol;\n}\n\nexport interface ClientQueueData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: MatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: ClientQueueData, matchGroup: MatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class RankedQueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: MatchGroup[] = [];\n highPriorityGroups: MatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: MatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave();\n }\n },\n }\n\n onCreate(options: RankedQueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"RankedQueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"RankedQueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: Client, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: Client, queueData: ClientQueueData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: MatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: ClientQueueData }>[]) {\n const teamsByID: { [teamId: string | symbol]: MatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: MatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: ClientQueueData }>[],\n currentGroup: MatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Room
|
|
1
|
+
import { Room } from '../Room.ts';
|
|
2
|
+
import type { Client } from '../Transport.ts';
|
|
3
|
+
import type { IRoomCache } from '../matchmaker/driver.ts';
|
|
2
4
|
export interface RankedQueueOptions {
|
|
3
5
|
/**
|
|
4
6
|
* number of players on each match
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// packages/core/src/rooms/RankedQueueRoom.ts
|
|
2
|
-
import { Room
|
|
2
|
+
import { Room } from "../Room.mjs";
|
|
3
|
+
import * as matchMaker from "../MatchMaker.mjs";
|
|
4
|
+
import { debugMatchMaking } from "../Debug.mjs";
|
|
5
|
+
import { ServerError } from "../errors/ServerError.mjs";
|
|
6
|
+
import { ErrorCode } from "../Protocol.mjs";
|
|
3
7
|
var DEFAULT_TEAM = /* @__PURE__ */ Symbol("$default_team");
|
|
4
8
|
var DEFAULT_COMPARE = (client, matchGroup) => {
|
|
5
9
|
const diff = Math.abs(client.rank - matchGroup.averageRank);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/rooms/RankedQueueRoom.ts"],
|
|
4
|
-
"sourcesContent": ["import { Room, type Client, matchMaker, type IRoomCache, debugMatchMaking, ServerError, ErrorCode } from \"@colyseus/core\";\n\nexport interface RankedQueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: ClientQueueData, matchGroup: MatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: RankedQueueRoom, group: MatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface MatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: ClientQueueData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface MatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: ClientQueueData }>>,\n teamId: string | symbol;\n}\n\nexport interface ClientQueueData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: MatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: ClientQueueData, matchGroup: MatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class RankedQueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: MatchGroup[] = [];\n highPriorityGroups: MatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: MatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave();\n }\n },\n }\n\n onCreate(options: RankedQueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"RankedQueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"RankedQueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: Client, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: Client, queueData: ClientQueueData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: MatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: ClientQueueData }>[]) {\n const teamsByID: { [teamId: string | symbol]: MatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: MatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: ClientQueueData }>[],\n currentGroup: MatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
|
|
5
|
-
"mappings": ";AAAA,SAAS,
|
|
4
|
+
"sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { ErrorCode } from '../Protocol.ts';\n\nexport interface RankedQueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: ClientQueueData, matchGroup: MatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: RankedQueueRoom, group: MatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface MatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: ClientQueueData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface MatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: ClientQueueData }>>,\n teamId: string | symbol;\n}\n\nexport interface ClientQueueData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: MatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: ClientQueueData, matchGroup: MatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class RankedQueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: MatchGroup[] = [];\n highPriorityGroups: MatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: MatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave();\n }\n },\n }\n\n onCreate(options: RankedQueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"RankedQueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"RankedQueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: Client, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: Client, queueData: ClientQueueData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: MatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: ClientQueueData }>[]) {\n const teamsByID: { [teamId: string | symbol]: MatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: MatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: ClientQueueData }>[],\n currentGroup: MatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/router/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Server } from \"http\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint } from \"@colyseus/better-call\";\nimport { toNodeHandler } from \"@colyseus/better-call/node\";\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n} from \"@colyseus/better-call\";\n\nexport {
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,yBAAsH;AACtH,kBAA8B;AAE9B,IAAAA,
|
|
4
|
+
"sourcesContent": ["import type { Server } from \"http\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint } from \"@colyseus/better-call\";\nimport { toNodeHandler } from \"@colyseus/better-call/node\";\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export types needed for declaration emit\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToServer(server: Server, router: Router) {\n // check if the server is bound to an express app\n const expressApp: any = server.listeners('request').find((listener: Function) =>\n listener.name === \"app\" && listener['mountpath'] === '/');\n\n if (expressApp) {\n // bind the router to the express app\n expressApp.use(toNodeHandler(router.handler));\n\n } else {\n // otherwise, bind the router to the http server\n server.on('request', toNodeHandler(router.handler));\n }\n}\n\n/**\n * Do not use this directly. This is used internally by `@colyseus/playground`.\n * TODO: refactor. Avoid using globals.\n * @internal\n */\nexport let __globalEndpoints: Record<string, Endpoint> = {};\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config?: Config) {\n // TODO: refactor. Avoid using globals.\n __globalEndpoints = endpoints;\n\n return createBetterCallRouter({ ...endpoints, }, config);\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,yBAAsH;AACtH,kBAA8B;AAE9B,IAAAA,sBAYO;AAIA,SAAS,mBAAmB,QAAgB,QAAgB;AAEjE,QAAM,aAAkB,OAAO,UAAU,SAAS,EAAE,KAAK,CAAC,aACxD,SAAS,SAAS,SAAS,SAAS,WAAW,MAAM,GAAG;AAE1D,MAAI,YAAY;AAEd,eAAW,QAAI,2BAAc,OAAO,OAAO,CAAC;AAAA,EAE9C,OAAO;AAEL,WAAO,GAAG,eAAW,2BAAc,OAAO,OAAO,CAAC;AAAA,EACpD;AACF;AAOO,IAAI,oBAA8C,CAAC;AAEnD,SAAS,aAGd,WAAc,QAAiB;AAE/B,sBAAoB;AAEpB,aAAO,mBAAAC,cAAuB,EAAE,GAAG,UAAW,GAAG,MAAM;AACzD;",
|
|
6
6
|
"names": ["import_better_call", "createBetterCallRouter"]
|
|
7
7
|
}
|
package/build/router/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Server } from "http";
|
|
2
2
|
import { type Endpoint, type Router, type RouterConfig } from "@colyseus/better-call";
|
|
3
3
|
import { toNodeHandler } from "@colyseus/better-call/node";
|
|
4
|
-
export { createEndpoint, createMiddleware, createInternalContext, } from "@colyseus/better-call";
|
|
5
|
-
export {
|
|
4
|
+
export { createEndpoint, createMiddleware, createInternalContext, type Router, type RouterConfig, type Endpoint, type EndpointOptions, type EndpointContext, type StrictEndpoint, } from "@colyseus/better-call";
|
|
5
|
+
export { toNodeHandler };
|
|
6
6
|
export declare function bindRouterToServer(server: Server, router: Router): void;
|
|
7
7
|
/**
|
|
8
8
|
* Do not use this directly. This is used internally by `@colyseus/playground`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/router/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Server } from \"http\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint } from \"@colyseus/better-call\";\nimport { toNodeHandler } from \"@colyseus/better-call/node\";\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n} from \"@colyseus/better-call\";\n\nexport {
|
|
5
|
-
"mappings": ";AACA,SAAwD,gBAAgB,8BAA8C;AACtH,SAAS,qBAAqB;AAE9B;AAAA,EACE,kBAAAA;AAAA,EACA;AAAA,EACA;AAAA,
|
|
4
|
+
"sourcesContent": ["import type { Server } from \"http\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint } from \"@colyseus/better-call\";\nimport { toNodeHandler } from \"@colyseus/better-call/node\";\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export types needed for declaration emit\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToServer(server: Server, router: Router) {\n // check if the server is bound to an express app\n const expressApp: any = server.listeners('request').find((listener: Function) =>\n listener.name === \"app\" && listener['mountpath'] === '/');\n\n if (expressApp) {\n // bind the router to the express app\n expressApp.use(toNodeHandler(router.handler));\n\n } else {\n // otherwise, bind the router to the http server\n server.on('request', toNodeHandler(router.handler));\n }\n}\n\n/**\n * Do not use this directly. This is used internally by `@colyseus/playground`.\n * TODO: refactor. Avoid using globals.\n * @internal\n */\nexport let __globalEndpoints: Record<string, Endpoint> = {};\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config?: Config) {\n // TODO: refactor. Avoid using globals.\n __globalEndpoints = endpoints;\n\n return createBetterCallRouter({ ...endpoints, }, config);\n}\n"],
|
|
5
|
+
"mappings": ";AACA,SAAwD,gBAAgB,8BAA8C;AACtH,SAAS,qBAAqB;AAE9B;AAAA,EACE,kBAAAA;AAAA,EACA;AAAA,EACA;AAAA,OASK;AAIA,SAAS,mBAAmB,QAAgB,QAAgB;AAEjE,QAAM,aAAkB,OAAO,UAAU,SAAS,EAAE,KAAK,CAAC,aACxD,SAAS,SAAS,SAAS,SAAS,WAAW,MAAM,GAAG;AAE1D,MAAI,YAAY;AAEd,eAAW,IAAI,cAAc,OAAO,OAAO,CAAC;AAAA,EAE9C,OAAO;AAEL,WAAO,GAAG,WAAW,cAAc,OAAO,OAAO,CAAC;AAAA,EACpD;AACF;AAOO,IAAI,oBAA8C,CAAC;AAEnD,SAAS,aAGd,WAAc,QAAiB;AAE/B,sBAAoB;AAEpB,SAAO,uBAAuB,EAAE,GAAG,UAAW,GAAG,MAAM;AACzD;",
|
|
6
6
|
"names": ["createEndpoint"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colyseus/core",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.10",
|
|
4
4
|
"description": "Multiplayer Framework for Node.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"input": "./src/index.ts",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"@standard-schema/spec": "^1.0.0",
|
|
51
51
|
"debug": "^4.3.4",
|
|
52
52
|
"nanoid": "^3.3.11",
|
|
53
|
+
"@colyseus/shared-types": "^0.17.0",
|
|
53
54
|
"@colyseus/better-call": "^1.0.26",
|
|
54
55
|
"@colyseus/greeting-banner": "^3.0.6"
|
|
55
56
|
},
|
package/src/MatchMaker.ts
CHANGED
|
@@ -24,21 +24,14 @@ import { logger } from './Logger.ts';
|
|
|
24
24
|
import type { AuthContext, Client } from './Transport.ts';
|
|
25
25
|
import { getLockId, initializeRoomCache, type ExtractMetadata } from './matchmaker/driver.ts';
|
|
26
26
|
|
|
27
|
+
export type { ISeatReservation } from '@colyseus/shared-types';
|
|
28
|
+
import type { ISeatReservation } from '@colyseus/shared-types';
|
|
29
|
+
|
|
27
30
|
export { controller, stats, type MatchMakerDriver };
|
|
28
31
|
|
|
29
32
|
export type ClientOptions = any;
|
|
30
33
|
export type SelectProcessIdCallback = (roomName: string, clientOptions: ClientOptions) => Promise<string>;
|
|
31
34
|
|
|
32
|
-
export interface ISeatReservation {
|
|
33
|
-
name: string;
|
|
34
|
-
sessionId: string;
|
|
35
|
-
roomId: string;
|
|
36
|
-
publicAddress?: string;
|
|
37
|
-
processId?: string;
|
|
38
|
-
reconnectionToken?: string;
|
|
39
|
-
devMode?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
35
|
const handlers: {[id: string]: RegisteredHandler} = {};
|
|
43
36
|
const rooms: {[roomId: string]: Room} = {};
|
|
44
37
|
const events = new EventEmitter();
|