build config changes
All checks were successful
Galvanic Corrosion Cross-Compile / build (push) Successful in 1m50s
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:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user