/* Galvanic Corrosion - Rec Room custom server for communities. Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import Rooms from "../content/rooms.ts"; import { RoomDataTypes } from "../content/rooms/DataTypes.ts"; import { Profile } from "../profiles.ts"; import Instances from "./instances.ts"; import { MatchmakingErrorCode, RoomInstance } from "./types.ts"; const loginLocks: Map = new Map(); interface MatchmakingOptions { roomName: string, subRoomName?: string, private?: boolean, instanceId?: number, profile: Profile } interface MatchmakingResponse { errorCode: MatchmakingErrorCode, roomInstance?: RoomInstance } class MatchmakingBase { createLoginLock(prof: Profile, lock: string) { if (loginLocks.has(prof.getId())) return; else loginLocks.set(prof.getId(), lock); } lockMatches(prof: Profile, lock: string) { const checkLock = loginLocks.get(prof.getId()); if (checkLock) return checkLock == lock; else return null; } deleteLoginLock(prof: Profile) { loginLocks.delete(prof.getId()); } async matchmake(options: MatchmakingOptions): Promise { if (options.instanceId) { const instance = Instances.getInstance(options.instanceId); if (instance) { if (Instances.playerIsInInstance(options.profile, instance)) return { errorCode: MatchmakingErrorCode.AlreadyInTargetInstance }; else { if (instance.isFull) return { errorCode: MatchmakingErrorCode.InsufficientSpace } else if (instance.isPrivate) return { errorCode: MatchmakingErrorCode.RoomInstanceIsPrivate }; await Instances.setPlayerInstance(options.profile, instance); Instances.clearAllRoomEmptyInstances(instance.roomId); return { errorCode: MatchmakingErrorCode.Success, roomInstance: instance }; } } else return { errorCode: MatchmakingErrorCode.NoSuchGame } } else { // check to make sure room exists, is not private, and is active const targetRoom = options.roomName !== 'DormRoom' ? await Rooms.getByName(options.roomName) : await Rooms.getProfileDormDefault(options.profile); if (!targetRoom) return { errorCode: MatchmakingErrorCode.NoSuchRoom }; if (targetRoom.Room.Accessibility == RoomDataTypes.RoomAccessibility.Private && targetRoom.Room.CreatorPlayerId !== options.profile.getId()) return { errorCode: MatchmakingErrorCode.RoomIsPrivate }; if (targetRoom.Room.State !== RoomDataTypes.RoomState.Active) return { errorCode: MatchmakingErrorCode.RoomIsNotActive }; const roomId = targetRoom.Room.RoomId; Instances.clearAllRoomEmptyInstances(roomId); // get all available instance let allInstances = Instances.getAllRoomInstances(roomId).values().toArray().filter(instance => !instance.isPrivate && !instance.isFull); const subroomId = targetRoom.Scenes.find(scene => scene.Name == options.subRoomName)?.RoomSceneId; if (subroomId) allInstances = allInstances.filter(instance => instance.subRoomId == subroomId); // filter instances that do not support join in progress and are in progress const builtinRooms = Rooms.getAllBuiltinRooms(); const joinInProgressSubrooms = builtinRooms.map(room => ({Name: room.Name, Scenes: room.Scenes.map(scene => ({Name: scene.Name, Supported: scene.SupportsJoinInProgress}) )}) ); allInstances = allInstances.filter(instance => { const subroomName = Rooms.getSubroomNameFromId(targetRoom, instance.subRoomId); if (!subroomName) return false; const subrooms = joinInProgressSubrooms.find(room => room.Name == targetRoom.Room.Name); if (!subrooms) return false; const supportsJoinInProgress = subrooms.Scenes.find(subroom => subroom.Name == subroomName)?.Supported; if (supportsJoinInProgress) return true; else if (!instance.isInProgress) return true; else return false; }); allInstances = allInstances.sort((a, b) => { const pidsA = Instances.getInstancePlayers(a); const pidsB = Instances.getInstancePlayers(b); return pidsA.size - pidsB.size; }).reverse(); // Largest instances first const foundInstance = allInstances[0]; if (!foundInstance) { const matchmakeableSubrooms = targetRoom.Scenes.filter(scene => scene.CanMatchmakeInto); const newInstance = await Instances.createInstance({ Room: targetRoom, SceneIndex: Math.floor(Math.random() * matchmakeableSubrooms.length), FirstPlayer: options.profile, Private: options.private, IsDorm: options.roomName == 'DormRoom' }); Instances.clearAllRoomEmptyInstances(roomId); return { errorCode: MatchmakingErrorCode.Success, roomInstance: newInstance }; } else { const currentInstance = options.profile.getInstance(); if (currentInstance?.roomInstanceId == foundInstance.roomInstanceId) return { errorCode: MatchmakingErrorCode.AlreadyInBestInstance }; await Instances.setPlayerInstance(options.profile, foundInstance); Instances.clearAllRoomEmptyInstances(roomId); return { errorCode: MatchmakingErrorCode.Success, roomInstance: foundInstance }; } } } } const Matchmaking = new MatchmakingBase(); export default Matchmaking;