UNPKG

@colyseus/core

Version:

Multiplayer Framework for Node.js.

241 lines (239 loc) 9.11 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // packages/core/src/rooms/QueueRoom.ts var QueueRoom_exports = {}; __export(QueueRoom_exports, { QueueRoom: () => QueueRoom }); module.exports = __toCommonJS(QueueRoom_exports); var import_Room = require("../Room.cjs"); var matchMaker = __toESM(require("../MatchMaker.cjs"), 1); var import_Debug = require("../Debug.cjs"); var import_ServerError = require("../errors/ServerError.cjs"); var import_shared_types = require("@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 import_Room.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(import_shared_types.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 import_ServerError.ServerError(import_shared_types.ErrorCode.APPLICATION_ERROR, "QueueRoom: 'matchRoomName' option is required."); } (0, import_Debug.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); } }); } }); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { QueueRoom });