build config changes
All checks were successful
Galvanic Corrosion Cross-Compile / build (push) Successful in 1m50s

* Commit hash shipped with builds
* Post & pre-build events
* Objective fixes
* Orientation challenge filler
* Custom Rooms base
    - Currently cannot save rooms (CDN not set up)
* Moved root path to path.ts
* Room cloning
* Rewrote instances - the whole thing
* Relationships are still untested
* Charades Words
* AG Room fetch
* Private room matchmaking
* Socket fixes
This commit is contained in:
2025-05-12 09:07:59 -04:00
parent 6a249ef813
commit 83440a9245
96 changed files with 1201 additions and 436 deletions

View File

@@ -1,6 +1,6 @@
/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
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
@@ -21,84 +21,191 @@ 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";
const log = new Logging("Instances");
const config = Config.getConfig();
const instancePlayers: Map<RoomInstance, Set<Profile>> = new Map();
/**
* `Map<roomId (number), RoomInstance>`
* `Map<roomId (number), Instance>`
*/
const instanceMap: Map<number, Set<RoomInstance>> = new Map();
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 did 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 = UnifiedProfile.get(options.Room.Room.CreatorPlayerId);
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);
}
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 = instancePlayers.keys();
const instances = instanceSet.values().toArray();
const instance = instances.find(val => val.roomInstanceId == id);
if (instance) return instance;
else return null;
}
getAllInstances() {
return new Set([...instanceMap.values()].flatMap(set => [...set]));
return new Set([...instanceSet.values().toArray()]);
}
getAllRoomInstances(roomId: number) {
let instances = instanceMap.get(roomId);
if (!instances) {
instances = new Set();
instanceMap.set(roomId, instances);
}
return instances;
}
getInstancePlayers(instance: RoomInstance): Set<Profile> {
let players = instancePlayers.get(instance);
if (!players) {
players = new Set();
instancePlayers.set(instance, players);
}
return players;
return new Set([...this.getAllInstances().values().toArray().filter(val => val.roomId == roomId)]);
}
clearEmptyInstances(instances: Set<RoomInstance>, roomId?: number) {
const beforeCount = instances.size;
for (const instance of instances) {
const profiles = this.getInstancePlayers(instance);
if (profiles.size === 0) {
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`);
instancePlayers.delete(instance);
this.getAllRoomInstances(instance.roomId).delete(instance);
instance.destroy();
}
}
const afterCount = instances.size;
log.d(`Cleared ${roomId !== undefined ? `room ${roomId}` : 'all'} empty instances.\n Instances before: ${beforeCount}\n Instances after: ${afterCount}`);
}
clearAllEmptyInstances() {
this.clearEmptyInstances(this.getAllInstances());
}
clearAllRoomEmptyInstances(roomId: number) {
this.clearEmptyInstances(this.getAllRoomInstances(roomId), roomId);
}
updateGlobalInstancesIsFull() {
for (const instance of this.getAllInstances())
instance.isFull = this.getInstancePlayers(instance).size >= instance.maxCapacity;
}
updateSingleInstanceIsFull(instance: RoomInstance) {
const profiles = this.getInstancePlayers(instance);
if (profiles.size >= instance.maxCapacity) instance.isFull = true;
else instance.isFull = false;
}
instanceCanFitPlayer(instance: RoomInstance) {
return this.getInstancePlayers(instance).size < instance.maxCapacity;
const afterCount = instanceSet.size;
log.d(`Cleared empty instances for roomId ${roomId ? roomId : "*"}.\n Instances before: ${beforeCount}\n Instances after: ${afterCount}`);
}
#generateUniqueInstanceId() {
@@ -112,96 +219,19 @@ class InstancesBase {
* Create an instance with options.
*
* If `options.FirstPlayer` is not specified, the created instance will not contain any players and may be removed.
*
* If one is, the player will be automatically added to the instance and their `profile.getInstance()` will be synchronized.
*/
async createInstance(options: InstanceOptions) {
const scene = options.Room.Scenes[options.SceneIndex];
const newId = this.#generateUniqueInstanceId();
if (!scene) throw new Error("The specified scene did not exist.");
const newInstance = await new Instance(newId).init(options);
let instanceName = scene.Name === "Home" || scene.Name === options.Room.Room.Name ? `^${options.Room.Room.Name}` : `^${options.Room.Room.Name}.${scene.Name}`;
if (options.IsDorm) {
const dormCreatorPlayer = UnifiedProfile.get(options.Room.Room.CreatorPlayerId);
const player = await dormCreatorPlayer.export();
if (player) instanceName = `@${player.displayName}'s Dorm`;
}
const newInstance: RoomInstance = {
roomInstanceId: newId,
roomId: options.Room.Room.RoomId,
subRoomId: scene.RoomSceneId,
location: scene.RoomSceneLocationId,
dataBlob: scene.DataBlobName,
eventId: options.EventId,
photonRegionId: config.public.photonRegionId,
photonRoomId: `20200306-GC${newId}`,
name: instanceName,
maxCapacity: scene.MaxPlayers,
isFull: false,
isPrivate: typeof options.Private !== 'boolean' ? false : options.Private,
isInProgress: false
};
this.getAllRoomInstances(options.Room.Room.RoomId).add(newInstance);
if (options.FirstPlayer) {
this.setPlayerInstance(options.FirstPlayer, newInstance);
this.getInstancePlayers(newInstance).add(options.FirstPlayer);
}
instanceSet.add(newInstance);
if (options.FirstPlayer)
newInstance.addPlayer(options.FirstPlayer);
return newInstance;
}
/**
* Call only when the player is ready to be moved to an instance
*
* Synchronizes profile instance to `instance` and adds player to instance.
*/
async setPlayerInstance(player: Profile, instance: RoomInstance) {
const currentInstance = player.getInstance();
if (currentInstance === instance) return;
if (currentInstance) {
this.getInstancePlayers(currentInstance).delete(player);
this.updateSingleInstanceIsFull(currentInstance);
}
if (this.instanceCanFitPlayer(instance)) {
const instancePlayers = this.getInstancePlayers(instance);
log.i(`Player ${player.getId()} went to '${instance.name}' with ${instancePlayers.size} other players`);
instancePlayers.add(player);
player.setInstance(instance);
const pres = await Presence.get(player);
pres.update();
this.updateSingleInstanceIsFull(instance);
} else log.w(`Instance ${instance.roomInstanceId} is full. Cannot add player ${player.getId()}`);
const room = await new RoomFactory({ id: instance.roomId }).init();
await room?.addVisit();
}
playerIsInInstance(player: Profile, instance: RoomInstance) {
const profiles = this.getInstancePlayers(instance);
return profiles.has(player);
}
/**
* Call only when the player is ready to be removed (or when not responding)
*
* Synchronizes profile instance to `null` and removes player from instance.
*/
removePlayerFromCurrentInstance(player: Profile) {
const instance = player.getInstance();
if (!instance) return;
this.getInstancePlayers(instance).delete(player);
player.setInstance(null);
this.updateSingleInstanceIsFull(instance);
}
}