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