Some checks failed
Galvanic Corrosion Cross-Compile / build (push) Failing after 38s
* Rewrite rooms backend, "RoomFactory" and "SubroomFactory"
- Used for modifying and fetching rooms
* Progression and reputation bulk endpoints
* Announcement endpoint temp
* OOBE is now the only initial pref key
- Will be removed in the future when cohortnux is implemented
* Misc minor fixes and clarifications
* Simplified namegen dictionary
- The previous one was generated with ChatGPT, hence the duplicated strings. I googled "random username generator" and borrowed a random result's generation dictionary.
* QuickPlay support with "initialRoom" in config (untested)
153 lines
6.5 KiB
TypeScript
153 lines
6.5 KiB
TypeScript
/* Galvanic Corrosion - Rec Room custom server for communities.
|
|
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
|
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 <https://www.gnu.org/licenses/>. */
|
|
|
|
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<number, string> = 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<MatchmakingResponse> {
|
|
|
|
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; |