forked from zombieb/galvanic-corrosion-rewrite
354 lines
16 KiB
TypeScript
354 lines
16 KiB
TypeScript
import Logging from "@proxnet/undead-logging";
|
|
import type KV from "../../persistence/kv.ts";
|
|
import { type ServerBase } from "../../server.ts";
|
|
import { DatabaseRoom, FactoryMode, GalvanicTagDTO, HardwareSupports, RoomAccessibility, RoomDataTypes, RoomState, TagDTO, TagType, WriteMode } from "./RoomDataTypes.ts";
|
|
import { SubroomFactory } from "./SubroomFactory.ts";
|
|
import { ServerRoomsBase } from "../base.ts";
|
|
|
|
const log = new Logging("RoomFactory");
|
|
|
|
const roomDebug = false;
|
|
|
|
interface FactoryOptionsBase {
|
|
mode: FactoryMode,
|
|
id: number,
|
|
name?: string
|
|
}
|
|
export interface WriteFactoryOptions extends FactoryOptionsBase {
|
|
mode: FactoryMode.Write,
|
|
writeMode: WriteMode,
|
|
}
|
|
export interface FetchFactoryOptions extends FactoryOptionsBase {
|
|
mode: FactoryMode.Fetch
|
|
}
|
|
export type FactoryOptions = WriteFactoryOptions | FetchFactoryOptions;
|
|
|
|
export class RoomFactory {
|
|
|
|
static roomsKey = "rooms";
|
|
|
|
#server: ServerBase;
|
|
#kv: KV;
|
|
|
|
#roomId: number | undefined;
|
|
|
|
factoryMode: FactoryMode = FactoryMode.Fetch;
|
|
writeMode: WriteMode = WriteMode.WriteIfFree;
|
|
|
|
#obj: DatabaseRoom | null = null;
|
|
#subrooms: Set<number> | null = null;
|
|
#hardwareSupport: Set<RoomDataTypes.HardwareSupport> | null = null;
|
|
#tags: Set<string> | null = null;
|
|
|
|
#cannotAccessBeforeInitError = new Error("Cannot access properties before initialization");
|
|
#cannotWriteBeforeInitError = new Error("Cannot write before initialization");
|
|
|
|
constructor(server: ServerBase, kv: KV) {
|
|
|
|
this.#server = server;
|
|
this.#kv = kv;
|
|
|
|
}
|
|
|
|
/**
|
|
* Initialize the factory. Retrieves the room from the database and populates factory values.
|
|
*
|
|
* Does not fetch subroom content, only available subroom IDs.
|
|
*
|
|
* When using write mode, values are initialized to defaults.
|
|
*
|
|
* Defaults:
|
|
* - All hardware is supported
|
|
* - Cloning is not allowed
|
|
* - Room is not AG or dorm
|
|
* - State is Moderation_Closed
|
|
* - Accessibility is Unlisted
|
|
* - CreatorPlayerId is 1 (Coach)
|
|
* - Name and Description are empty strings
|
|
*/
|
|
async init(options: FactoryOptions) {
|
|
if (typeof options.id == 'undefined' && typeof options.name == 'undefined')
|
|
throw new Error("Must specify a room ID or a room name");
|
|
|
|
if (typeof options.id !== 'undefined') this.#roomId = options.id;
|
|
else {
|
|
if (!options.name) throw new Error("Room name must be provided");
|
|
|
|
const id = await this.#server.Rooms.getIdFromName(options.name);
|
|
if (id == null) throw new Error("Room name not found");
|
|
this.#roomId = id;
|
|
}
|
|
|
|
const obj = await this.#kv.getKv().get<DatabaseRoom>([RoomFactory.roomsKey, this.#roomId, 'meta']);
|
|
if (options.mode == FactoryMode.Fetch && obj.value == null) return null;
|
|
else this.#obj = options.mode == FactoryMode.Fetch ? obj.value : {
|
|
Hardware: new Set(HardwareSupports),
|
|
Tags: new Set(),
|
|
CoOwners: new Set(),
|
|
InvitedCoOwners: new Set(),
|
|
Hosts: new Set(),
|
|
InvitedHosts: new Set(),
|
|
Room: {
|
|
Name: "",
|
|
Description: "",
|
|
CreatorPlayerId: 1,
|
|
ImageName: "",
|
|
State: RoomState.Moderation_Closed,
|
|
Accessibility: RoomAccessibility.Unlisted,
|
|
SupportsLevelVoting: false,
|
|
IsAGRoom: false,
|
|
IsDormRoom: false,
|
|
CloningAllowed: false,
|
|
AllowsJuniors: true,
|
|
RoomWarningMask: 0,
|
|
CustomRoomWarning: "",
|
|
DisableMicAutoMute: null
|
|
}
|
|
};
|
|
|
|
const subrooms = await this.#kv.getKv().get<Set<number>>([RoomFactory.roomsKey, this.#roomId, "subrooms"]);
|
|
if (options.mode == FactoryMode.Write || subrooms.value == null) this.#subrooms = new Set();
|
|
else this.#subrooms = subrooms.value;
|
|
|
|
const hardwareSupport = await this.#kv.getKv().get<Set<RoomDataTypes.HardwareSupport>>([RoomFactory.roomsKey, this.#roomId, 'hardware']);
|
|
if (hardwareSupport.value == null) this.#hardwareSupport = new Set(HardwareSupports);
|
|
else this.#hardwareSupport = hardwareSupport.value;
|
|
|
|
const tags = await this.#kv.getKv().get<Set<string>>([RoomFactory.roomsKey, this.#roomId, 'tags']);
|
|
this.#tags = tags.value;
|
|
|
|
this.factoryMode = options.mode;
|
|
if (options.mode == FactoryMode.Write) this.writeMode = options.writeMode;
|
|
|
|
return this;
|
|
}
|
|
|
|
async write() {
|
|
if (typeof this.#roomId !== 'number') throw this.#cannotWriteBeforeInitError;
|
|
|
|
const w = async () => {
|
|
if (typeof this.#roomId !== 'number') throw this.#cannotWriteBeforeInitError;
|
|
|
|
await Promise.all([
|
|
this.#kv.getKv().set([RoomFactory.roomsKey, this.#roomId, 'meta'], this.#obj),
|
|
this.#kv.getKv().set([RoomFactory.roomsKey, this.#roomId, 'hardware'], this.#hardwareSupport),
|
|
this.#kv.getKv().set([RoomFactory.roomsKey, this.#roomId, 'tags'], this.#tags),
|
|
this.#kv.getKv().set([RoomFactory.roomsKey, this.#roomId, "subrooms"], this.#subrooms)
|
|
]);
|
|
|
|
if (!this.IsDormRoom) this.#kv.getKv().set([ServerRoomsBase.roomNamesKey, this.Name], this.#roomId);
|
|
else this.#kv.getKv().set([ServerRoomsBase.playerDormsKey, this.CreatorPlayerId], this.#roomId);
|
|
|
|
this.#server.emit('room.updated', { room: this });
|
|
if (roomDebug) log.d(`Room ${this.#roomId} written and emitted`);
|
|
}
|
|
|
|
if (this.factoryMode == FactoryMode.Fetch) throw new Error("Cannot write in fetch mode");
|
|
|
|
const key = [RoomFactory.roomsKey, this.#roomId, 'meta'];
|
|
const val = await this.#kv.getKv().get(key);
|
|
if (val.value == null) await w();
|
|
else {
|
|
if (this.writeMode == WriteMode.Overwrite) await w();
|
|
else throw new Error("Room already exists");
|
|
}
|
|
}
|
|
|
|
async export(): Promise<RoomDataTypes.RoomDetails> {
|
|
if (!this.#obj || !this.#roomId) throw this.#cannotAccessBeforeInitError;
|
|
const obj = this.#obj;
|
|
const hardwareSupport = this.getHardwareSupport();
|
|
const autoTags = this.getTags();
|
|
const galvTags = this.getGalvanicTags();
|
|
|
|
const subroomExports = (await Promise.all(
|
|
this.getSubrooms().values().map(subroom => this.getSubroom(subroom))
|
|
)).map(factory => factory.export());
|
|
|
|
return {
|
|
Room: {
|
|
RoomId: this.#roomId,
|
|
Name: obj.Room.Name,
|
|
Description: obj.Room.Description,
|
|
CreatorPlayerId: obj.Room.CreatorPlayerId,
|
|
ImageName: obj.Room.ImageName,
|
|
State: obj.Room.State,
|
|
Accessibility: obj.Room.Accessibility,
|
|
SupportsLevelVoting: obj.Room.SupportsLevelVoting,
|
|
IsAGRoom: obj.Room.IsAGRoom,
|
|
IsDormRoom: obj.Room.IsDormRoom,
|
|
CloningAllowed: obj.Room.CloningAllowed,
|
|
AllowsJuniors: obj.Room.AllowsJuniors,
|
|
RoomWarningMask: obj.Room.RoomWarningMask,
|
|
CustomRoomWarning: obj.Room.CustomRoomWarning,
|
|
DisableMicAutoMute: obj.Room.DisableMicAutoMute,
|
|
SupportsScreens: hardwareSupport.has('screens'),
|
|
SupportsWalkVR: hardwareSupport.has('walk_vr'),
|
|
SupportsTeleportVR: hardwareSupport.has('teleport_vr'),
|
|
SupportsMobile: hardwareSupport.has('mobile'),
|
|
SupportsVRLow: hardwareSupport.has('low_vr')
|
|
},
|
|
Scenes: subroomExports,
|
|
Tags: autoTags.concat(galvTags),
|
|
CoOwners: [],
|
|
InvitedCoOwners: [],
|
|
Hosts: [],
|
|
InvitedHosts: [],
|
|
CheerCount: 0,
|
|
FavoriteCount: 0,
|
|
VisitCount: await this.getVisitCount()
|
|
}
|
|
}
|
|
setRoomProperties(props: RoomDataTypes.DatabaseRoomContent) {
|
|
Object.assign(this, props);
|
|
}
|
|
|
|
getRoomId() {
|
|
if (!this.#roomId) throw this.#cannotAccessBeforeInitError;
|
|
return this.#roomId;
|
|
}
|
|
|
|
getSubrooms() {
|
|
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
|
return this.#subrooms;
|
|
}
|
|
async getSubroom(id: number) {
|
|
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
|
if (!this.#subrooms.has(id)) throw new Error("Subroom not available to this room");
|
|
return await new SubroomFactory(this.#server, this.#kv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
|
|
}
|
|
getAvailableSubroomId() {
|
|
let id = Math.round(Math.random() * Math.pow(2, 31));
|
|
if (this.getSubrooms().has(id)) id = this.getAvailableSubroomId();
|
|
return id;
|
|
}
|
|
async newSubroom(mode?: WriteMode) {
|
|
return await new SubroomFactory(this.#server, this.#kv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: this.getAvailableSubroomId() });
|
|
}
|
|
addSubroom(id: number) {
|
|
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
|
this.#subrooms.add(id);
|
|
}
|
|
|
|
get Name() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.Name }
|
|
set Name(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.Name = data }
|
|
|
|
get Description() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.Description }
|
|
set Description(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.Description = data }
|
|
|
|
get CreatorPlayerId() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.CreatorPlayerId }
|
|
set CreatorPlayerId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.CreatorPlayerId = data }
|
|
|
|
get ImageName() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.ImageName }
|
|
set ImageName(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.ImageName = data }
|
|
|
|
get State() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.State }
|
|
set State(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.State = data }
|
|
|
|
get Accessibility() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.Accessibility }
|
|
set Accessibility(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.Accessibility = data }
|
|
|
|
get SupportsLevelVoting() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.SupportsLevelVoting }
|
|
set SupportsLevelVoting(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.SupportsLevelVoting = data }
|
|
|
|
get IsAGRoom() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.IsAGRoom }
|
|
set IsAGRoom(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.IsAGRoom = data }
|
|
|
|
get IsDormRoom() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.IsDormRoom }
|
|
set IsDormRoom(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.IsDormRoom = data }
|
|
|
|
get CloningAllowed() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.CloningAllowed }
|
|
set CloningAllowed(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.CloningAllowed = data }
|
|
|
|
get AllowsJuniors() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.AllowsJuniors }
|
|
set AllowsJuniors(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.AllowsJuniors = data }
|
|
|
|
get RoomWarningMask() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.RoomWarningMask }
|
|
set RoomWarningMask(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.RoomWarningMask = data }
|
|
|
|
get CustomRoomWarning() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.CustomRoomWarning }
|
|
set CustomRoomWarning(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.CustomRoomWarning = data }
|
|
|
|
get DisableMicAutoMute() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.DisableMicAutoMute }
|
|
set DisableMicAutoMute(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.DisableMicAutoMute = data }
|
|
|
|
getHardwareSupport() {
|
|
if (!this.#hardwareSupport) throw this.#cannotAccessBeforeInitError;
|
|
return this.#hardwareSupport;
|
|
}
|
|
async #saveHardwareSupport() {
|
|
if (!this.#roomId) throw this.#cannotAccessBeforeInitError;
|
|
await this.#kv.getKv().set([this.#roomId, 'hardware'], this.#hardwareSupport);
|
|
}
|
|
async addHardwareSupport(...hardware: RoomDataTypes.HardwareSupport[]) {
|
|
if (!this.#hardwareSupport || !this.#roomId) throw this.#cannotAccessBeforeInitError;
|
|
|
|
if (typeof hardware == 'string') {
|
|
this.#hardwareSupport.add(hardware);
|
|
await this.#saveHardwareSupport();
|
|
} else {
|
|
for (const hw of hardware) this.#hardwareSupport.add(hw);
|
|
await this.#saveHardwareSupport();
|
|
}
|
|
}
|
|
async removeHardwareSupport(hardware: RoomDataTypes.HardwareSupport) {
|
|
if (!this.#hardwareSupport) throw this.#cannotAccessBeforeInitError;
|
|
this.#hardwareSupport.delete(hardware);
|
|
await this.#saveHardwareSupport();
|
|
}
|
|
/**
|
|
* Removes support for every hardware type in the room. When saving immediately after this, no client can legally join the room.
|
|
*
|
|
* In production, ensure at least one hardware type is supported.
|
|
*/
|
|
removeAllHardwareSupport() {
|
|
this.#hardwareSupport = new Set();
|
|
}
|
|
/**
|
|
* Adds support for every hardware type in the room.
|
|
*/
|
|
addAllHardwareSupport() {
|
|
this.#hardwareSupport = new Set(HardwareSupports);
|
|
}
|
|
|
|
/**
|
|
* Visits are fetched during every access, not during init
|
|
*/
|
|
async getVisitCount() {
|
|
if (!this.#roomId) throw this.#cannotAccessBeforeInitError;
|
|
const val = await this.#kv.getKv().get<number>([RoomFactory.roomsKey, this.#roomId, 'visits']);
|
|
if (val.value == null) return 0;
|
|
else return val.value;
|
|
}
|
|
/**
|
|
* Visits are fetched during every access, not during init
|
|
*/
|
|
async addVisit() {
|
|
if (!this.#roomId) throw this.#cannotAccessBeforeInitError;
|
|
const visits = await this.getVisitCount();
|
|
await this.#kv.getKv().set([RoomFactory.roomsKey, this.#roomId, 'visits'], visits + 1);
|
|
}
|
|
|
|
getGalvanicTags(): GalvanicTagDTO[] {
|
|
const tags: GalvanicTagDTO[] = [];
|
|
|
|
if (this.IsAGRoom) tags.push({ Tag: "recroomoriginal", Type: TagType.AGOnly });
|
|
if (this.IsDormRoom) tags.push({ Tag: "dormroom", Type: TagType.AGOnly });
|
|
if (this.Name === "Paintball" || this.Name === "PaintballVR") tags.push({ Tag: "paintball", Type: TagType.AGOnly });
|
|
const hardwareSupport = this.getHardwareSupport();
|
|
if (hardwareSupport.has('screens')) tags.push({ Tag: "screen", Type: TagType.Auto });
|
|
if (hardwareSupport.has('walk_vr')) tags.push({ Tag: "walkvr", Type: TagType.Auto });
|
|
if (hardwareSupport.has('teleport_vr')) tags.push({ Tag: "teleportvr", Type: TagType.Auto });
|
|
if (this.AllowsJuniors) tags.push({ Tag: "junior", Type: TagType.Auto });
|
|
|
|
return tags;
|
|
}
|
|
getTags(): TagDTO[] {
|
|
if (!this.#tags) return [];
|
|
return this.#tags.values().toArray().map(val => ({ Tag: val, Type: TagType.General }));
|
|
}
|
|
setTags(tags: Set<string>) {
|
|
this.#tags = tags;
|
|
}
|
|
|
|
} |