@colyseus/core 0.17.29 → 0.17.30
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/Room.cjs.map +2 -2
- package/build/Room.mjs.map +2 -2
- package/build/rooms/LobbyRoom.cjs +2 -1
- package/build/rooms/LobbyRoom.cjs.map +2 -2
- package/build/rooms/LobbyRoom.mjs +2 -1
- package/build/rooms/LobbyRoom.mjs.map +2 -2
- package/build/rooms/QueueRoom.cjs +1 -1
- package/build/rooms/QueueRoom.cjs.map +2 -2
- package/build/rooms/QueueRoom.mjs +2 -2
- package/build/rooms/QueueRoom.mjs.map +2 -2
- package/package.json +5 -5
- package/src/Room.ts +0 -1
- package/src/rooms/LobbyRoom.ts +4 -1
- package/src/rooms/QueueRoom.ts +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/rooms/QueueRoom.ts"],
|
|
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 '@colyseus/shared-types';\n\nexport interface QueueOptions {\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: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\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?: QueueMatchGroup;\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\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n userData: QueueClientData;\n messages: {\n clients: number;\n seat: matchMaker.ISeatReservation;\n }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\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 QueueRoom 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: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => 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: QueueOptions) {\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, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#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: QueueClient, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: QueueClient, queueData: QueueClientData) {\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: QueueMatchGroup = { 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: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\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: QueueMatchGroup = 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: QueueClientData }>[],\n currentGroup: QueueMatchGroup = 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;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAqB;AAGrB,iBAA4B;AAC5B,mBAAiC;AACjC,yBAA4B;AAC5B,
|
|
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 { CloseCode, ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\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: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\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?: QueueMatchGroup;\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\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n userData: QueueClientData;\n messages: {\n clients: number;\n seat: matchMaker.ISeatReservation;\n }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\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 QueueRoom 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: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => 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(CloseCode.NORMAL_CLOSURE);\n }\n },\n }\n\n onCreate(options: QueueOptions) {\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, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#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: QueueClient, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: QueueClient, queueData: QueueClientData) {\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: QueueMatchGroup = { 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: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\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: QueueMatchGroup = 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: QueueClientData }>[],\n currentGroup: QueueMatchGroup = 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
|
}
|
|
@@ -3,7 +3,7 @@ import { Room } from "../Room.mjs";
|
|
|
3
3
|
import * as matchMaker from "../MatchMaker.mjs";
|
|
4
4
|
import { debugMatchMaking } from "../Debug.mjs";
|
|
5
5
|
import { ServerError } from "../errors/ServerError.mjs";
|
|
6
|
-
import { ErrorCode } from "@colyseus/shared-types";
|
|
6
|
+
import { CloseCode, ErrorCode } from "@colyseus/shared-types";
|
|
7
7
|
var DEFAULT_TEAM = /* @__PURE__ */ Symbol("$default_team");
|
|
8
8
|
var DEFAULT_COMPARE = (client, matchGroup) => {
|
|
9
9
|
const diff = Math.abs(client.rank - matchGroup.averageRank);
|
|
@@ -34,7 +34,7 @@ var QueueRoom = class extends Room {
|
|
|
34
34
|
if (queueData && queueData.group && typeof queueData.group.confirmed === "number") {
|
|
35
35
|
queueData.confirmed = true;
|
|
36
36
|
queueData.group.confirmed++;
|
|
37
|
-
client.leave();
|
|
37
|
+
client.leave(CloseCode.NORMAL_CLOSURE);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/rooms/QueueRoom.ts"],
|
|
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 '@colyseus/shared-types';\n\nexport interface QueueOptions {\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: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\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?: QueueMatchGroup;\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\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n userData: QueueClientData;\n messages: {\n clients: number;\n seat: matchMaker.ISeatReservation;\n }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\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 QueueRoom 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: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => 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: QueueOptions) {\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, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#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: QueueClient, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: QueueClient, queueData: QueueClientData) {\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: QueueMatchGroup = { 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: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\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: QueueMatchGroup = 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: QueueClientData }>[],\n currentGroup: QueueMatchGroup = 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,YAAY;AAGrB,YAAY,gBAAgB;AAC5B,SAAS,wBAAwB;AACjC,SAAS,mBAAmB;AAC5B,SAAS,iBAAiB;
|
|
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 { CloseCode, ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\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: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\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?: QueueMatchGroup;\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\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n userData: QueueClientData;\n messages: {\n clients: number;\n seat: matchMaker.ISeatReservation;\n }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\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 QueueRoom 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: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => 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(CloseCode.NORMAL_CLOSURE);\n }\n },\n }\n\n onCreate(options: QueueOptions) {\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, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#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: QueueClient, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: QueueClient, queueData: QueueClientData) {\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: QueueMatchGroup = { 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: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\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: QueueMatchGroup = 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: QueueClientData }>[],\n currentGroup: QueueMatchGroup = 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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colyseus/core",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.30",
|
|
4
4
|
"description": "Multiplayer Framework for Node.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"input": "./src/index.ts",
|
|
@@ -51,16 +51,16 @@
|
|
|
51
51
|
"@standard-schema/spec": "^1.0.0",
|
|
52
52
|
"debug": "^4.3.4",
|
|
53
53
|
"nanoid": "^3.3.11",
|
|
54
|
-
"@colyseus/shared-types": "^0.17.
|
|
54
|
+
"@colyseus/shared-types": "^0.17.6",
|
|
55
55
|
"@colyseus/better-call": "^1.2.1",
|
|
56
56
|
"@colyseus/greeting-banner": "^3.0.8"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@colyseus/schema": "^4.0.7",
|
|
60
60
|
"express": "^5.0.0",
|
|
61
|
-
"@colyseus/
|
|
61
|
+
"@colyseus/redis-driver": "^0.17.6",
|
|
62
62
|
"@colyseus/redis-presence": "^0.17.6",
|
|
63
|
-
"@colyseus/
|
|
63
|
+
"@colyseus/tools": "^0.17.18"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"@colyseus/schema": "^4.0.7",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"express": "^4.16.0 || ^5.0.0",
|
|
69
69
|
"zod": "^4.1.12",
|
|
70
70
|
"@colyseus/better-call": "^1.2.1",
|
|
71
|
-
"@colyseus/ws-transport": "^0.17.
|
|
71
|
+
"@colyseus/ws-transport": "^0.17.9"
|
|
72
72
|
},
|
|
73
73
|
"optionalPeerDependencies": {
|
|
74
74
|
"@colyseus/tools": "0.17.x",
|
package/src/Room.ts
CHANGED
|
@@ -620,7 +620,6 @@ export class Room<T extends RoomOptions = RoomOptions> {
|
|
|
620
620
|
|
|
621
621
|
const client = this.clients.find((client) => client.reconnectionToken === reconnectionToken);
|
|
622
622
|
if (client) {
|
|
623
|
-
// this.allowReconnection(client, this.seatReservationTimeout);
|
|
624
623
|
this.#_forciblyCloseClient(client as ExtractRoomClient<T> & ClientPrivate, CloseCode.WITH_ERROR);
|
|
625
624
|
return client.sessionId;
|
|
626
625
|
}
|
package/src/rooms/LobbyRoom.ts
CHANGED
|
@@ -103,7 +103,10 @@ export class LobbyRoom<Metadata = any> extends Room {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
public onJoin(client: LobbyClient, options: LobbyOptions) {
|
|
106
|
-
this.clientOptions[client.sessionId] =
|
|
106
|
+
this.clientOptions[client.sessionId] = (
|
|
107
|
+
!Array.isArray(options) && // Defold (Lua) sends empty objects as Array instead of object
|
|
108
|
+
options
|
|
109
|
+
) || {};
|
|
107
110
|
client.send('rooms', this.filterItemsForClient(this.clientOptions[client.sessionId]));
|
|
108
111
|
}
|
|
109
112
|
|
package/src/rooms/QueueRoom.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { IRoomCache } from '../matchmaker/driver.ts';
|
|
|
4
4
|
import * as matchMaker from '../MatchMaker.ts';
|
|
5
5
|
import { debugMatchMaking } from '../Debug.ts';
|
|
6
6
|
import { ServerError } from '../errors/ServerError.ts';
|
|
7
|
-
import { ErrorCode } from '@colyseus/shared-types';
|
|
7
|
+
import { CloseCode, ErrorCode } from '@colyseus/shared-types';
|
|
8
8
|
|
|
9
9
|
export interface QueueOptions {
|
|
10
10
|
/**
|
|
@@ -160,7 +160,7 @@ export class QueueRoom extends Room {
|
|
|
160
160
|
if (queueData && queueData.group && typeof (queueData.group.confirmed) === "number") {
|
|
161
161
|
queueData.confirmed = true;
|
|
162
162
|
queueData.group.confirmed++;
|
|
163
|
-
client.leave();
|
|
163
|
+
client.leave(CloseCode.NORMAL_CLOSURE);
|
|
164
164
|
}
|
|
165
165
|
},
|
|
166
166
|
}
|