@colyseus/core
Version:
Multiplayer Framework for Node.js.
241 lines (239 loc) • 9.11 kB
JavaScript
"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
});