import Logging from "@proxnet/undead-logging"; import type KV from "../../persistence/kv.ts"; import { type ServerBase } from "../../server.ts"; import { DatabaseSubroom, FactoryMode, RoomDataTypes, RoomSave, RoomSaveMap, WriteMode } from "./RoomDataTypes.ts"; export interface SubroomFactoryOptions { mode: FactoryMode, writeMode: WriteMode id: number } const log = new Logging("SubroomFactoryBase"); export class SubroomFactory { #server: ServerBase; #kv: KV; #subroomId: number | null = null; factoryMode: FactoryMode = FactoryMode.Fetch; writeMode: WriteMode = WriteMode.WriteIfFree; #obj: DatabaseSubroom | null = null; #saves: RoomSaveMap | 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; } async init(options: SubroomFactoryOptions) { const data = await this.#kv.getKv().get([options.id, 'meta']); if (options.mode == FactoryMode.Fetch && data == null) throw new Error("No such subroom"); const saves = await this.#kv.getKv().get([options.id, 'saves']); this.#saves = saves.value ?? new Map(); this.#obj = options.mode == FactoryMode.Fetch ? data.value : { RoomId: 0, RoomSceneLocationId: "", Name: "Subroom data init failed, contact an admin!", IsSandbox: false, MaxPlayers: 8, CanMatchmakeInto: true, LatestSaveId: null }; // use template object when writing this.#subroomId = options.id; return this; } async write() { if (!this.#obj) throw this.#cannotWriteBeforeInitError; if (!this.#subroomId) throw new Error("No RoomSceneId set"); await this.#kv.getKv().set([this.#subroomId, 'meta'], this.#obj); await this.#kv.getKv().set([this.#subroomId, 'saves'], this.#saves); this.#server.emit('room.subroom.updated', { subroom: this }); } export(): RoomDataTypes.RoomScene { if (!this.#subroomId) throw this.#cannotAccessBeforeInitError; const save = this.getLatestSave(); if (!save) throw new Error("No save to export"); return { RoomId: this.RoomId, RoomSceneId: this.#subroomId, RoomSceneLocationId: this.RoomSceneLocationId, Name: this.Name, IsSandbox: this.IsSandbox, MaxPlayers: this.MaxPlayers, CanMatchmakeInto: this.CanMatchmakeInto, DataModifiedAt: save.SavedAt.toISOString(), DataBlobName: save.DataBlobName } } setSubroomProperties(props: RoomDataTypes.SubroomProps) { Object.assign(this, props); } get RoomId() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.RoomId; } set RoomId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.RoomId = data } get RoomSceneId() { if (!this.#subroomId) throw this.#cannotAccessBeforeInitError; else return this.#subroomId; } get RoomSceneLocationId() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.RoomSceneLocationId; } set RoomSceneLocationId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.RoomSceneLocationId = data } get Name() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Name; } set Name(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.Name = data } get IsSandbox() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.IsSandbox; } set IsSandbox(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.IsSandbox = data } get MaxPlayers() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.MaxPlayers; } set MaxPlayers(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.MaxPlayers = data } get CanMatchmakeInto() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.CanMatchmakeInto; } set CanMatchmakeInto(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.CanMatchmakeInto = data } get LatestSaveId() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.LatestSaveId; } set LatestSaveId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.LatestSaveId = data } #getAvailableSaveId() { if (!this.#saves) throw this.#cannotAccessBeforeInitError; let newId = Math.round(Math.random() * Math.pow(2, 31)); while (this.#saves.has(newId)) newId = this.#getAvailableSaveId(); return newId; } /** * Add a save (history) to the scene. Automatically resets the LatestSaveId. * * Use empty string for no datablob * * @param dataBlobName Filename of datablob on CDN */ addSave(dataBlobName?: string) { if (!this.#saves) throw this.#cannotAccessBeforeInitError; const newId = this.#getAvailableSaveId(); this.#saves.set(newId, { DataBlobName: dataBlobName ?? "", SavedAt: new Date() }); this.LatestSaveId = newId; } getSaves() { if (!this.#saves) throw this.#cannotAccessBeforeInitError; return this.#saves; } getLatestSave() { if (!this.#saves || !this.#obj) throw this.#cannotAccessBeforeInitError; else if (!this.#obj.LatestSaveId) throw new Error(`No save is marked as the latest save`); else { if (this.#saves.size === 0) { log.w(`No save could be found when fetching the latest save for subroomid ${this.#subroomId}!`); return null; } else if (this.#saves.size === 1) return this.#saves.values().toArray()[0]; else return this.#saves.get(this.#obj.LatestSaveId); } } }