This repository has been archived on 2026-03-19. You can view files and clone it, but cannot push or open issues or pull requests.
Files
galvanic-corrosion/src/data/live/instances.ts
zombieb 648d46986c
All checks were successful
Galvanic Corrosion Cross-Compile / build (push) Successful in 49s
Custom Rooms + Server global
* Added Storage and room saving (will be moved to events later)
* Moved `UnifiedProfile` to new `Server` object, along with `CDN`
    - Will move `Rooms` and others to this later
2025-05-24 21:20:30 -04:00

245 lines
8.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 Logging from "@proxnet/undead-logging";
import { Profile } from "../profiles.ts";
import { RoomInstance, InstanceOptions } from "./types.ts";
import { Config } from "../../config.ts";
import Presence from "./presence.ts";
import { RoomFactory } from "../content/rooms/RoomFactory.ts";
import { RoomDataTypes } from "../content/rooms/DataTypes.ts";
import { PushNotificationId } from "../../socket/types.ts";
import Server from "../server.ts";
const log = new Logging("Instances");
const config = Config.getConfig();
/**
* `Map<roomId (number), Instance>`
*/
const instanceSet: Set<Instance> = new Set();
export class Instance {
#players = new Set<Profile>();
timeCreated = new Date().toISOString();
#id: number;
#room: RoomDataTypes.RoomDetails | undefined;
#subroom: RoomDataTypes.RoomScene | undefined;
#eventId?: number; // not yet implemented
#name?: string;
#priv?: boolean;
#inProgress?: boolean;
#blob?: string;
constructor(id: number) {
this.#id = id;
}
async init(options: InstanceOptions) {
const scene = options.Room.Scenes[options.SceneIndex];
if (!scene) throw new Error("The specified scene does not exist.");
let instanceName;
if (scene.Name == 'Home' || scene.Name === options.Room.Room.Name) instanceName = `^${options.Room.Room.Name}`;
else instanceName = `^${options.Room.Room.Name}.${scene.Name}`;
if (options.IsDorm) {
const dormCreatorPlayer = Server.UnifiedProfile.get(options.Room.Room.CreatorPlayerId);
if (!dormCreatorPlayer) throw new Error("Creator of dorm does not exist.");
const player = await dormCreatorPlayer.export();
if (player) instanceName = `@${player.displayName}'s Dorm`;
}
this.#room = options.Room;
this.#subroom = scene;
this.#name = instanceName;
this.#blob = scene.DataBlobName;
this.#inProgress = false;
this.#priv = options.Private ? options.Private : false;
return this;
}
equalInstance(instance: RoomInstance) {
return instance.roomInstanceId == this.#id;
}
getAllPlayers() {
return this.#players.values().toArray();
}
hasPlayer(player: Profile) {
return this.getAllPlayers().includes(player);
}
removePlayer(player: Profile) {
if (!this.hasPlayer(player)) throw new Error(`Cannot remove player ${player.getId()} from instance ${this.#id} they are not in`);
this.#players.delete(player);
player.setInstance(null);
}
updatePlayers() {
for (const player of this.#players.values()) player.getSocketHandler()?.sendNotification(PushNotificationId.SubscriptionUpdateGameSession, this.snapshot());
}
async addPlayer(player: Profile) {
const currentInstance = player.getInstance();
if (currentInstance && currentInstance.equalInstance(this)) return;
if (currentInstance) currentInstance.removePlayer(player);
if (!this.isFull) {
const instancePlayers = this.getAllPlayers();
const profileExport = await player.export();
log.i(`Player ${player.getId()} "${profileExport?.displayName}" went to '${this.name}' with ${instancePlayers.length} other players`);
this.#players.add(player);
player.setInstance(this);
const pres = await Presence.get(player);
pres.update();
const room = await new RoomFactory({ id: this.roomId }).init();
await room?.addVisit();
// move some of this to a dedicated "onPlayerMove" function
} else log.w(`Instance ${this.roomInstanceId} is full. Cannot add player ${player.getId()}`);
log.d(`Players in instance ${this.#id}: ${this.#players.values().toArray().map(prof => prof.getId()).join(',')}`);
}
get roomInstanceId() { return this.#id }
get roomId() { return this.#room ? this.#room?.Room.RoomId : 0 }
get subRoomId() { return this.#subroom ? this.#subroom?.RoomSceneId : 0 }
get location() { return this.#subroom ? this.#subroom?.RoomSceneLocationId : "" }
get dataBlob() { return this.#blob ? this.#blob : undefined }
set dataBlob(data) { this.#blob = data }
get eventId() { return this.#eventId }
get photonRegionId() { return config.public.photonRegionId }
get photonRoomId() { return `GC20200306-${this.#id}` }
get name() { return this.#name ? this.#name : "InstanceNameError" }
get maxCapacity() { return this.#subroom ? this.#subroom.MaxPlayers : 8 }
get isFull() { return this.#players.size >= this.maxCapacity }
get isPrivate() { return this.#priv ? this.#priv : false }
set isPrivate(data) { this.#priv = data }
get isInProgress() { return this.#inProgress ? this.#inProgress : false }
set isInProgress(data) { this.#inProgress = data }
snapshot() {
const inst: RoomInstance = {
roomInstanceId: this.roomInstanceId,
roomId: this.roomId,
subRoomId: this.subRoomId,
location: this.location,
dataBlob: this.dataBlob,
eventId: this.eventId,
photonRegionId: this.photonRegionId,
photonRoomId: this.photonRoomId,
name: this.name,
maxCapacity: this.maxCapacity,
isFull: this.isFull,
isPrivate: this.isPrivate,
isInProgress: this.isInProgress
}
return inst;
}
destroy() {
instanceSet.delete(this);
if (this.#players.size !== 0) for (const player of this.#players) player.getSocketHandler()?.sendNotification(PushNotificationId.Logout);
}
}
class InstancesBase {
getInstance(id: number) {
const instances = instanceSet.values().toArray();
const instance = instances.find(val => val.roomInstanceId == id);
if (instance) return instance;
else return null;
}
getAllInstances() {
return new Set([...instanceSet.values().toArray()]);
}
getAllRoomInstances(roomId: number) {
return new Set([...this.getAllInstances().values().toArray().filter(val => val.roomId == roomId)]);
}
clearEmptyInstances(roomId?: number) {
const beforeCount = instanceSet.size;
for (const instance of instanceSet) {
if (roomId && instance.roomId == roomId) continue;
const profiles = instance.getAllPlayers();
if (profiles.length === 0) {
log.i(`Instance ${instance.roomInstanceId} empty, deleting`);
instance.destroy();
}
}
const afterCount = instanceSet.size;
log.d(`Cleared empty instances for roomId ${roomId ? roomId : "*"}.\n Instances before: ${beforeCount}\n Instances after: ${afterCount}`);
}
#generateUniqueInstanceId() {
let newInstanceId = Math.round(Math.random() * Math.pow(2, 31));
const allInstances = this.getAllInstances();
while (Array.from(allInstances.values()).map(val => val.roomInstanceId).includes(newInstanceId)) newInstanceId = Math.round(Math.random() * Math.pow(2, 31));
return newInstanceId;
}
/**
* Create an instance with options.
*
* If `options.FirstPlayer` is not specified, the created instance will not contain any players and may be removed.
*/
async createInstance(options: InstanceOptions) {
const newId = this.#generateUniqueInstanceId();
const newInstance = await new Instance(newId).init(options);
instanceSet.add(newInstance);
if (options.FirstPlayer)
newInstance.addPlayer(options.FirstPlayer);
return newInstance;
}
}
const Instances = new InstancesBase();
export default Instances;