UNPKG

@colyseus/core

Version:

Multiplayer Framework for Node.js.

206 lines (205 loc) 7.39 kB
// packages/core/src/rooms/QueueRoom.ts import { Room } from "../Room.mjs"; import * as matchMaker from "../MatchMaker.mjs"; import { debugMatchMaking } from "../Debug.mjs"; import { ServerError } from "../errors/ServerError.mjs"; import { CloseCode, ErrorCode } from "@colyseus/shared-types"; var DEFAULT_TEAM = /* @__PURE__ */ Symbol("$default_team"); var DEFAULT_COMPARE = (client, matchGroup) => { const diff = Math.abs(client.rank - matchGroup.averageRank); const diffRatio = diff / matchGroup.averageRank; return diff < 10 || diffRatio <= 2; }; var QueueRoom = class extends Room { constructor() { super(...arguments); this.maxPlayers = 4; this.allowIncompleteGroups = false; this.maxWaitingCycles = 15; this.maxWaitingCyclesForPriority = 10; /** * Evaluate groups for each client at interval */ this.cycleTickInterval = 1e3; /** * Groups of players per iteration */ this.groups = []; this.highPriorityGroups = []; this.compare = DEFAULT_COMPARE; this.onGroupReady = (group) => matchMaker.createRoom(this.matchRoomName, {}); this.messages = { confirm: (client, _) => { const queueData = client.userData; if (queueData && queueData.group && typeof queueData.group.confirmed === "number") { queueData.confirmed = true; queueData.group.confirmed++; client.leave(CloseCode.NORMAL_CLOSURE); } } }; } onCreate(options) { if (typeof options.maxWaitingCycles === "number") { this.maxWaitingCycles = options.maxWaitingCycles; } if (typeof options.maxPlayers === "number") { this.maxPlayers = options.maxPlayers; } if (typeof options.maxTeamSize === "number") { this.maxTeamSize = options.maxTeamSize; } if (typeof options.allowIncompleteGroups !== "undefined") { this.allowIncompleteGroups = options.allowIncompleteGroups; } if (typeof options.compare === "function") { this.compare = options.compare; } if (typeof options.onGroupReady === "function") { this.onGroupReady = options.onGroupReady; } if (options.matchRoomName) { this.matchRoomName = options.matchRoomName; } else { throw new ServerError(ErrorCode.APPLICATION_ERROR, "QueueRoom: 'matchRoomName' option is required."); } debugMatchMaking("QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName); this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval); } onJoin(client, options, auth) { this.addToQueue(client, { rank: options.rank, teamId: options.teamId, options }); } addToQueue(client, queueData) { if (queueData.currentCycle === void 0) { queueData.currentCycle = 0; } client.userData = queueData; client.send("clients", 1); } createMatchGroup() { const group = { clients: [], averageRank: 0 }; this.groups.push(group); return group; } reassignMatchGroups() { this.groups.length = 0; this.highPriorityGroups.length = 0; const sortedClients = this.clients.filter((client) => { return client.userData && client.userData.group?.ready !== true; }).sort((a, b) => { return a.userData.rank - b.userData.rank; }); if (typeof this.maxTeamSize === "number") { this.redistributeTeams(sortedClients); } else { this.redistributeClients(sortedClients); } this.evaluateHighPriorityGroups(); this.processGroupsReady(); } redistributeTeams(sortedClients) { const teamsByID = {}; sortedClients.forEach((client) => { const teamId = client.userData.teamId || DEFAULT_TEAM; if (!teamsByID[teamId]) { teamsByID[teamId] = { teamId, clients: [], averageRank: 0 }; } teamsByID[teamId].averageRank += client.userData.rank; teamsByID[teamId].clients.push(client); }); let teams = Object.values(teamsByID).map((team) => { team.averageRank /= team.clients.length; return team; }).sort((a, b) => { return a.averageRank - b.averageRank; }); do { let currentGroup = this.createMatchGroup(); teams = teams.filter((team) => { const totalRank = team.averageRank * team.clients.length; currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank); if (team.clients.length >= this.maxTeamSize) { return true; } team.clients.forEach((client) => client.userData.currentCycle++); return false; }); } while (teams.length >= 2); } redistributeClients(sortedClients, currentGroup = this.createMatchGroup(), totalRank = 0) { for (let i = 0, l = sortedClients.length; i < l; i++) { const client = sortedClients[i]; const userData = client.userData; const currentCycle = userData.currentCycle++; if (currentGroup.averageRank > 0) { if (!this.compare(userData, currentGroup) && !userData.highPriority) { currentGroup = this.createMatchGroup(); totalRank = 0; } } userData.group = currentGroup; currentGroup.clients.push(client); totalRank += userData.rank; currentGroup.averageRank = totalRank / currentGroup.clients.length; if (currentGroup.clients.length === this.maxPlayers) { currentGroup.ready = true; currentGroup = this.createMatchGroup(); totalRank = 0; continue; } if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) { if (this.highPriorityGroups.indexOf(currentGroup) === -1) { this.highPriorityGroups.push(currentGroup); } } else if (this.maxWaitingCyclesForPriority !== void 0 && currentCycle >= this.maxWaitingCyclesForPriority) { userData.highPriority = true; } } return currentGroup; } evaluateHighPriorityGroups() { this.highPriorityGroups.forEach((group) => { group.ready = group.clients.every((c) => { return c.userData?.currentCycle > 1; }); }); } processGroupsReady() { this.groups.forEach(async (group) => { if (group.ready) { group.confirmed = 0; try { const room = await this.onGroupReady.call(this, group); await matchMaker.reserveMultipleSeatsFor( room, group.clients.map((client) => ({ sessionId: client.sessionId, options: client.userData.options, auth: client.auth })) ); group.clients.forEach((client, i) => { client.send("seat", matchMaker.buildSeatReservation(room, client.sessionId)); }); } catch (e) { group.clients.forEach((client) => client.leave(1011, e.message)); } } else { group.clients.forEach((client) => { const queueClientCount = group.clients.length; if (client.userData.lastQueueClientCount !== queueClientCount) { client.userData.lastQueueClientCount = queueClientCount; client.send("clients", queueClientCount); } }); } }); } }; export { QueueRoom };