diff --git a/CONFIG.md b/CONFIG.md
index b4c67aa..42b7e6c 100644
--- a/CONFIG.md
+++ b/CONFIG.md
@@ -56,6 +56,9 @@ Ideally, this should be unique for every server, and can be chosen by the server
`photonRegionId`: The region to connect to when using Photon Cloud. When using the self-hosted PhotonSocketServer,
this can be anything *except* for "none" or 4, since there is only one server to connect to and the game uses offline mode when the region ID is set to none.
+`initialRoom`: On game startup, redirects the player to this room name instead of their DormRoom. Set to null if a "natural" startup is preferred.
+Ideally, this room should not be private and should be matchmakeable.
+
## Logging
These three values expose booleans you can change to enable/disable logging various messages sent by the server used for debugging or troubleshooting purposes.
diff --git a/res/roomgen.ts b/res/roomgen.ts
new file mode 100644
index 0000000..b267c0c
--- /dev/null
+++ b/res/roomgen.ts
@@ -0,0 +1,49 @@
+ DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163",
+ RecCenter = "cbad71af-0831-44d8-b8ef-69edafa841f6",
+ 3DCharades = "4078dfed-24bb-4db7-863f-578ba48d726b",
+ DiscGolfLake = "f6f7256c-e438-4299-b99e-d20bef8cf7e0",
+ DiscGolfPropulsion = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55",
+ Dodgeball = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
+ Paddleball = "d89f74fa-d51e-477a-a425-025a891dd499",
+ Paintball_River = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
+ Paintball_Homestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
+ Paintball_Quarry = "ff4c6427-7079-4f59-b22a-69b089420827",
+ Paintball_Clearcut = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
+ Paintball_Spillway = "58763055-2dfb-4814-80b8-16fac5c85709",
+ Paintball_Drive-in = "65ddbb48-5a01-4e3e-972d-e5c7419e2bc3",
+ PaintballVR_River = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
+ PaintballVR_Homestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
+ PaintballVR_Quarry = "ff4c6427-7079-4f59-b22a-69b089420827",
+ PaintballVR_Clearcut = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
+ PaintballVR_Spillway = "58763055-2dfb-4814-80b8-16fac5c85709",
+ GoldenTrophy = "91e16e35-f48f-4700-ab8a-a1b79e50e51b",
+ TheRiseofJumbotron = "acc06e66-c2d0-4361-b0cd-46246a4c455c",
+ CrimsonCauldron = "949fa41f-4347-45c0-b7ac-489129174045",
+ IsleOfLostSkulls = "7e01cfe0-820a-406f-b1b3-0a5bf575235c",
+ Soccer = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03",
+ LaserTag_Hangar = "239e676c-f12f-489f-bf3a-d4c383d692c3",
+ LaserTag_CyberJunkCity = "9d6456ce-6264-48b4-808d-2d96b3d91038",
+ RecRoyaleSquads = "253fa009-6e65-4c90-91a1-7137a56a267f",
+ RecRoyaleSolos = "b010171f-4875-4e89-baba-61e878cd41e1",
+ Lounge = "a067557f-ca32-43e6-b6e5-daaec60b4f5a",
+ PerformanceHall = "9932f88f-3929-43a0-a012-a40b5128e346",
+ MakerRoom = "a75f7547-79eb-47c6-8986-6767abcb4f92",
+ Park = "0a864c86-5a71-4e18-8041-8124e4dc9d98",
+ River = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
+ Homestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
+ Quarry = "ff4c6427-7079-4f59-b22a-69b089420827",
+ Clearcut = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
+ Spillway = "58763055-2dfb-4814-80b8-16fac5c85709",
+ Lake = "f6f7256c-e438-4299-b99e-d20bef8cf7e0",
+ PropulsionTestRange = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55",
+ Gym = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
+ Stadium = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03",
+ Hangar = "239e676c-f12f-489f-bf3a-d4c383d692c3",
+ CyberJunkCity = "9d6456ce-6264-48b4-808d-2d96b3d91038",
+ DodgeballVR = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
+ Crescendo = "49cb8993-a956-43e2-86f4-1318f279b22a",
+ Bowling = "ae929543-9a07-41d5-8ee9-dbbee8c36800",
+ BowlingAlley = "ae929543-9a07-41d5-8ee9-dbbee8c36800",
+ StuntRunner_StuntRunner = "b7281665-a715-4051-826b-8e08e69c6172",
+ StuntRunner_TheMainEvent = "3a636bd2-f896-424c-9225-c184522c0d87",
+ StuntRunnerBaseRoom = "882e9b96-7115-4b03-86f6-c0c9d8e22e00",
diff --git a/src/config.ts b/src/config.ts
index fd8d007..3a814be 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -50,6 +50,7 @@ type PublicConfiguration = {
maxLevels: number;
patches: string[];
photonRegionId: PhotonRegionCodeString | PhotonRegionCodeNumber;
+ initialRoom: string | null;
};
type LoggingConfiguration = {
@@ -112,6 +113,7 @@ export const defaultConfig: GalvanicConfiguration = {
maxLevels: 30,
patches: [],
photonRegionId: PhotonRegionCodeNumber.us,
+ initialRoom: null
},
logging: {
notfound: false,
diff --git a/src/data/content/room.ts b/src/data/content/room.ts
deleted file mode 100644
index 66f3158..0000000
--- a/src/data/content/room.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-/* Galvanic Corrosion - Rec Room custom server for communities.
-
-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 . */
-
-import { Redis } from "../../db.ts";
-import Rooms from "./rooms.ts";
-import { IntegratedRoomScene, RoomAccessibility, RoomDetails, RoomState } from "./roomtypes.ts";
-
-interface RoomFetchOptions {
- roomName?: string,
- roomId?: number
-}
-
-export function parseBooleanDefault(obj: string, def: boolean | undefined = false) {
- try {
- return JSON.parse(obj) as boolean;
- } catch {
- return def;
- }
-}
-
-export class RoomFetch {
-
- roomId: number | null = null;
- roomName: string | null = null;
-
- constructor(options: RoomFetchOptions) {
-
- this.roomId = options.roomId ?? null;
- this.roomName = options.roomName ?? null;
-
- }
-
- async fetch() {
-
- if (!this.roomId && this.roomName) {
- const givenId = await Rooms.getIdFromName(this.roomName);
- if (!givenId) return null;
- else this.roomId = givenId;
- } else if (!this.roomName && this.roomId) {
- const givenName = await Rooms.getNameFromId(this.roomId);
- if (!givenName) return null;
- else this.roomName = givenName;
- } else if (!this.roomId && !this.roomName) return null;
-
- const roomRootKey = Redis.buildKey(
- Redis.KeyGroups.Rooms.Root,
- this.roomId!.toString(), // code above takes care of null possibility
- );
- const roomMetaKey = Redis.buildKey(
- roomRootKey,
- Rooms.roomRootKeys.Meta
- );
-
- const [ hash, cheerCount, favoriteCount, visitCount ] = await Promise.all([
- Redis.Database.hgetall(roomMetaKey),
- Redis.Database.get(Redis.buildKey(roomRootKey, Rooms.roomRootKeys.CheerCount)),
- Redis.Database.get(Redis.buildKey(roomRootKey, Rooms.roomRootKeys.FavoriteCount)),
- Redis.Database.get(Redis.buildKey(roomRootKey, Rooms.roomRootKeys.VisitCount)),
- ]);
-
- const room: RoomDetails = {
- Room: {
- RoomId: hash[Rooms.roomMetaKeys.RoomId] ? parseInt(hash[Rooms.roomMetaKeys.RoomId]) : 0,
- Name: hash[Rooms.roomMetaKeys.Name] ?? "DATABASEERROR",
- Description: hash[Rooms.roomMetaKeys.Description] ?? "DATABASEERROR",
- CreatorPlayerId: hash[Rooms.roomMetaKeys.CreatorPlayerId] ? parseInt(hash[Rooms.roomMetaKeys.CreatorPlayerId]) : 1,
- ImageName: hash[Rooms.roomMetaKeys.ImageName] ?? "DefaultProfileImage.png",
- State: hash[Rooms.roomMetaKeys.State] ? parseInt(hash[Rooms.roomMetaKeys.State]) : RoomState.Active,
- Accessibility: hash[Rooms.roomMetaKeys.Accessibility] ? parseInt(hash[Rooms.roomMetaKeys.Accessibility]) : RoomAccessibility.Unlisted,
- SupportsLevelVoting: hash[Rooms.roomMetaKeys.SupportsLevelVoting] ? parseBooleanDefault(hash[Rooms.roomMetaKeys.SupportsLevelVoting], true) : true,
- IsAGRoom: hash[Rooms.roomMetaKeys.IsAGRoom] ? parseBooleanDefault(hash[Rooms.roomMetaKeys.IsAGRoom], false) : false,
- IsDormRoom: hash[Rooms.roomMetaKeys.IsDormRoom] ? parseBooleanDefault(hash[Rooms.roomMetaKeys.IsDormRoom], false) : false,
- CloningAllowed: hash[Rooms.roomMetaKeys.CloningAllowed] ? parseBooleanDefault(hash[Rooms.roomMetaKeys.CloningAllowed], false) : false,
- SupportsScreens: hash[Rooms.roomMetaKeys.SupportsScreens] ? parseBooleanDefault(hash[Rooms.roomMetaKeys.SupportsScreens], true) : true,
- SupportsWalkVR: hash[Rooms.roomMetaKeys.SupportsWalkVR] ? parseBooleanDefault(hash[Rooms.roomMetaKeys.SupportsWalkVR], true) : true,
- SupportsTeleportVR: hash[Rooms.roomMetaKeys.SupportsTeleportVR] ? parseBooleanDefault(hash[Rooms.roomMetaKeys.SupportsTeleportVR], true) : true,
- AllowsJuniors: hash[Rooms.roomMetaKeys.AllowsJuniors] ? parseBooleanDefault(hash[Rooms.roomMetaKeys.AllowsJuniors], true) : true,
- RoomWarningMask: hash[Rooms.roomMetaKeys.RoomWarningMask] ? parseInt(hash[Rooms.roomMetaKeys.RoomWarningMask]) : 0,
- CustomRoomWarning: hash[Rooms.roomMetaKeys.CustomRoomWarning] ?? "",
- DisableMicAutoMute: hash[Rooms.roomMetaKeys.DisableMicAutoMute] ? parseBooleanDefault(hash[Rooms.roomMetaKeys.DisableMicAutoMute], false) : undefined,
- },
- Scenes: [], // temporary
- CoOwners: [], // temporary
- InvitedCoOwners: [], // temporary
- Hosts: [], // temporary
- InvitedHosts: [], // temporary
- CheerCount: cheerCount ? parseInt(cheerCount) : 0,
- FavoriteCount: favoriteCount ? parseInt(favoriteCount) : 0,
- VisitCount: visitCount ? parseInt(visitCount) : 0,
- Tags: [] // temporary
- }
- const subrooms = await Rooms.getSubroomIdsFromRoom(this.roomId!);
- for (const subroom of subrooms) {
- const subroomMetaKey = Redis.buildKey(
- Redis.KeyGroups.Rooms.Root,
- this.roomId!.toString(),
- Rooms.roomRootKeys.Subrooms,
- subroom.toString(),
- Rooms.subroomRootKeys.Meta
- );
- const subroomDetails = await Redis.Database.hgetall(subroomMetaKey);
- room.Scenes.push({
- RoomSceneId: parseInt(subroom),
- RoomId: this.roomId ?? 0,
- RoomSceneLocationId: subroomDetails[Rooms.subroomMetaKeys.RoomSceneLocationId] ?? IntegratedRoomScene.MakerRoom,
- Name: subroomDetails[Rooms.subroomMetaKeys.Name] ?? "DATABASE ERROR",
- IsSandbox: subroomDetails[Rooms.subroomMetaKeys.IsSandbox] ? parseBooleanDefault(subroomDetails[Rooms.subroomMetaKeys.IsSandbox], false) : false,
- CanMatchmakeInto: subroomDetails[Rooms.subroomMetaKeys.IsSandbox] ? parseBooleanDefault(subroomDetails[Rooms.subroomMetaKeys.IsSandbox], true) : undefined,
- DataBlobName: subroomDetails[Rooms.subroomMetaKeys.DataBlobName] ?? "",
- MaxPlayers: subroomDetails[Rooms.subroomMetaKeys.MaxPlayers] ? parseInt(subroomDetails[Rooms.subroomMetaKeys.MaxPlayers]) : 1,
- DataModifiedAt: subroomDetails[Rooms.subroomMetaKeys.DataModifiedAt] ?? new Date().toISOString()
- });
- }
-
- return room;
- }
-
-}
\ No newline at end of file
diff --git a/src/data/content/rooms.ts b/src/data/content/rooms.ts
index 050e215..6644849 100644
--- a/src/data/content/rooms.ts
+++ b/src/data/content/rooms.ts
@@ -17,10 +17,12 @@ along with this program. If not, see . */
import { Redis } from "../../db.ts";
import { RootPath } from "./baseimages.ts";
-import { BuiltinRoom, RoomAccessibility, RoomDetails, RoomState } from "./roomtypes.ts";
-import { RoomFetch } from "./room.ts";
import { Profile } from "../profiles.ts";
import Logging from "@proxnet/undead-logging";
+import { BuiltinRoom, FactoryMode, IntegratedRoomScene, RoomAccessibility, RoomDetails, RoomState, WriteMode } from "./rooms/DataTypes.ts";
+import { RoomFactory } from "./rooms/RoomFactory.ts";
+import { SubroomFactory } from "./rooms/SubroomFactory.ts";
+import { Image } from "https://deno.land/x/imagescript@1.3.0/ImageScript.js";
const log = new Logging("Rooms");
@@ -28,51 +30,9 @@ const rooms = JSON.parse(Deno.readTextFileSync(`${RootPath}/res/rooms.json`)) as
class RoomsBase {
- readonly roomMetaKeys = { // hash keys
- RoomId: "id",
- Name: "name",
- Description: "desc",
- CreatorPlayerId: "creatorId",
- ImageName: "imagename",
- State: "state",
- Accessibility: "access",
- SupportsLevelVoting: "levelvoting",
- IsAGRoom: "isagroom",
- IsDormRoom: "isdorm",
- CloningAllowed: "cloneable",
- SupportsScreens: "can-screen",
- SupportsWalkVR: "can-walkvr",
- SupportsTeleportVR: "can-televr",
- AllowsJuniors: "juniors",
- RoomWarningMask: "warningmask",
- CustomRoomWarning: "warning",
- DisableMicAutoMute: "disableautomute"
- }
- readonly subroomMetaKeys = { // hash keys
- Name: "name",
- RoomSceneLocationId: "location",
- IsSandbox: "issandbox",
- CanMatchmakeInto: "matchmakeable",
- RoomSceneId: "sceneid",
- DataBlobName: "datablob",
- MaxPlayers: "playercap",
- DataModifiedAt: "modifiedat"
- }
- readonly roomRootKeys = {
- CheerCount: "cheers", // string
- CheerPids: "cheers-players", // set
- VisitCount: "visits", // string
- FavoriteCount: "favorites", // string
- FavoritePids: "favorites-players", // set
- Subrooms: "subrooms", // set
- Meta: "roommeta" // hash
- }
- readonly subroomRootKeys = {
- Meta: "scenemeta"
- }
- readonly miscKeys = {
+ static Keys = {
BuiltinGenerated: "builtinrooms-done",
- AGRooms: "agrooms"
+ AGRooms: "agrooms",
}
getAllBuiltinRooms() {
@@ -81,7 +41,9 @@ class RoomsBase {
async get(id: number) {
try {
- return await new RoomFetch({ roomId: id }).fetch();
+ const factory = await new RoomFactory({ id: id }).init();
+ if (!factory) return null;
+ return factory.export();
} catch {
return null;
}
@@ -89,21 +51,23 @@ class RoomsBase {
async getByName(name: string) {
try {
- return await new RoomFetch({ roomName: name }).fetch();
+ const factory = await new RoomFactory({ name: name }).init();
+ if (!factory) return null;
+ return factory.export();
} catch {
return null;
}
}
async getAllBuiltinRoomGenerations() {
- const ids = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.AGRooms));
+ const ids = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.AGRooms));
const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val));
return (await Promise.all(parsedIds.map(id => this.get(id)))).filter(val => val !== null);
}
async #getAvailableRoomId() {
let id = Math.round(Math.random() * Math.pow(2, 31));
- while ((await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Rooms.Root, id.toString(), this.roomRootKeys.Meta))) >= 1)
+ while ((await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Rooms.Root, id.toString(), RoomFactory.Keys.Meta))) >= 1)
id = await this.#getAvailableRoomId();
return id;
}
@@ -114,94 +78,76 @@ class RoomsBase {
Redis.buildKey(
Redis.KeyGroups.Rooms.Root,
roomid.toString(),
- this.roomRootKeys.Subrooms,
+ RoomFactory.Keys.Subrooms,
id.toString(),
- this.subroomRootKeys.Meta
+ SubroomFactory.Keys.Meta
))) >= 1)
id = await this.#getAvailableSubRoomId(roomid);
return id;
}
- async #setRoom(details: RoomDetails) {
- const rootKey = Redis.buildKey(Redis.KeyGroups.Rooms.Root, details.Room.RoomId.toString());
-
- const roomMetaRootKey = Redis.buildKey(rootKey, this.roomRootKeys.Meta);
- await Promise.all([
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.RoomId, details.Room.RoomId),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Name, details.Room.Name),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Description, details.Room.Description),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CreatorPlayerId, details.Room.CreatorPlayerId),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.ImageName, details.Room.ImageName),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.State, details.Room.State),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Accessibility, details.Room.Accessibility),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsLevelVoting, `${details.Room.SupportsLevelVoting}`),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.IsAGRoom, `${details.Room.IsAGRoom}`),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.IsDormRoom, `${details.Room.IsDormRoom}`),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CloningAllowed, `${details.Room.CloningAllowed}`),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsScreens, `${details.Room.SupportsScreens}`),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsWalkVR, `${details.Room.SupportsWalkVR}`),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsTeleportVR, `${details.Room.SupportsTeleportVR}`),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.AllowsJuniors, `${details.Room.AllowsJuniors}`),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.RoomWarningMask, details.Room.RoomWarningMask),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CustomRoomWarning, details.Room.CustomRoomWarning),
- Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.DisableMicAutoMute, `${details.Room.DisableMicAutoMute ? details.Room.DisableMicAutoMute : 'null'}`),
- ]);
-
- for (const subroom of details.Scenes) {
- const newSubId = await this.#getAvailableSubRoomId(details.Room.RoomId);
- const subRootMetaKey = Redis.buildKey(
- Redis.KeyGroups.Rooms.Root,
- details.Room.RoomId.toString(),
- this.roomRootKeys.Subrooms,
- newSubId.toString(),
- this.subroomRootKeys.Meta
- );
-
- await Promise.all([
- Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.RoomSceneLocationId, subroom.RoomSceneLocationId),
- Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.Name, subroom.Name),
- Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.IsSandbox, `${subroom.IsSandbox}`),
- Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.DataBlobName, subroom.DataBlobName),
- Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.MaxPlayers, subroom.MaxPlayers),
- Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.CanMatchmakeInto, `${subroom.CanMatchmakeInto}`),
- Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.DataModifiedAt, new Date().toISOString()),
- ]);
- await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, details.Room.RoomId.toString(), this.roomRootKeys.Subrooms), newSubId);
- }
-
- await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Room_Names, details.Room.Name), details.Room.RoomId);
- }
-
async cloneRoom(roomid: number, newname: string, newowner: Profile) {
- const canBeClonedRaw = await Redis.Database.hget(Redis.buildKey(
- Redis.KeyGroups.Rooms.Root,
- roomid.toString(),
- Rooms.roomRootKeys.Meta
- ), Rooms.roomMetaKeys.CloningAllowed);
- if (!canBeClonedRaw) return null;
- let canBeCloned = null;
- try {
- canBeCloned = JSON.parse(canBeClonedRaw) as boolean;
- } catch {
- log.d(`Cloneroom ${roomid}: parse error`);
- return null;
+ enum CloneResult {
+ Success,
+ DoesNotAllowCloning,
+ CannotCloneDormRoom,
+ NameIsTaken,
+ Unknown
}
- if (!canBeCloned) {
- log.d(`Cloneroom ${roomid}: cannot be cloned`);
- return null;
+ interface RoomClone {
+ factory?: RoomFactory;
+ result: CloneResult;
}
- const beforeRoom = await Rooms.get(roomid); // room must exist
- if (!beforeRoom || !beforeRoom.Room.CloningAllowed) return null; // room must be cloneable
- if (beforeRoom.Room.Name !== 'DormRoom' && await Rooms.getByName(newname)) return null; // room name cannot be taken
-
- const newId = await this.#getAvailableRoomId();
- beforeRoom.Room.CreatorPlayerId = newowner.getId();
- beforeRoom.Room.RoomId = newId;
- for (const subroom of beforeRoom.Scenes) subroom.RoomId = newId;
-
- await Rooms.#setRoom(beforeRoom);
- return beforeRoom;
+
+ const factory = await new RoomFactory({ id: roomid, factoryMode: FactoryMode.Fetch }).init();
+ if (!factory || !factory.CloningAllowed) return { result: CloneResult.DoesNotAllowCloning } as RoomClone;
+ if (factory.Name == 'DormRoom') return { result: CloneResult.CannotCloneDormRoom } as RoomClone;
+ if (factory.Name == newname) return { result: CloneResult.NameIsTaken } as RoomClone;
+ const newFactory = await new RoomFactory({ id: await Rooms.#getAvailableRoomId(), factoryMode: FactoryMode.Write }).init();
+ if (!newFactory) return { result: CloneResult.Unknown } as RoomClone;
+
+ newFactory.CreatorPlayerId = newowner.getId();
+ newFactory.Description = factory.Description;
+ newFactory.Name = factory.Name;
+ newFactory.ImageName = factory.Description;
+ newFactory.State = factory.State;
+ newFactory.RoomAccessibility = factory.RoomAccessibility;
+ newFactory.SupportsLevelVoting = factory.SupportsLevelVoting;
+ newFactory.IsAGRoom = factory.IsAGRoom;
+ newFactory.IsDormRoom = factory.IsDormRoom;
+ newFactory.CloningAllowed = false; // new rooms cannot be cloned
+ newFactory.AllowsJuniors = factory.AllowsJuniors;
+ newFactory.RoomWarningMask = factory.RoomWarningMask;
+ newFactory.CustomRoomWarning = factory.CustomRoomWarning;
+ newFactory.DisableMicAutoMute = factory.DisableMicAutoMute;
+
+ const oldHardware = await factory.getHardwareSupport();
+ const hardwarePromises = oldHardware.map(hw => newFactory.addHardwareSupport(hw));
+ await Promise.all(hardwarePromises);
+
+ const oldSubroomIds = await factory.getAllSubroomIds();
+ const promises = oldSubroomIds.map(async (id) => {
+ const newSubroomFactory = newFactory.getSubroom(id, FactoryMode.Write, WriteMode.Overwrite);
+ const oldSubroomFactory = factory.getSubroom(id, FactoryMode.Fetch);
+
+ newSubroomFactory.RoomSceneLocationId = oldSubroomFactory.RoomSceneLocationId;
+ newSubroomFactory.Name = oldSubroomFactory.Name;
+ newSubroomFactory.IsSandbox = oldSubroomFactory.IsSandbox;
+ newSubroomFactory.DataBlobName = oldSubroomFactory.DataBlobName;
+ newSubroomFactory.MaxPlayers = oldSubroomFactory.MaxPlayers;
+ newSubroomFactory.CanMatchmakeInto = oldSubroomFactory.CanMatchmakeInto;
+
+ await newSubroomFactory.write();
+ });
+ await Promise.all(promises);
+
+ await newFactory.write();
+
+ return {
+ factory: newFactory,
+ result: CloneResult.Success
+ } as RoomClone
}
async getProfileDormDefault(player: Profile) {
@@ -209,131 +155,127 @@ class RoomsBase {
Redis.KeyGroups.Rooms.Root,
Redis.KeyGroups.Rooms.PlayerDorms
), player.getId().toString());
+
if (unparsedId) {
log.d(`Unparsed dorm ID for profile ${player.getId()}: ${unparsedId}`);
const parsedId = parseInt(unparsedId);
- if (isNaN(parsedId)) {
- log.d(`Returning new dorm for profile ${player.getId()}`);
+ if (!isNaN(parsedId)) {
+ log.d(`Returning existing dorm for profile ${player.getId()}`);
return await Rooms.get(parsedId);
}
}
const newDorm = await this.generateNewDorm(player);
- await this.#setRoom(newDorm);
log.d(`New dorm for ${player.getId()} existed`);
if (!newDorm) return null;
log.d(`New dorm for ${player.getId()} was not null`);
+
await Redis.Database.hset(Redis.buildKey(
Redis.KeyGroups.Rooms.Root,
Redis.KeyGroups.Rooms.PlayerDorms
- ), player.getId().toString(), newDorm.Room.RoomId);
- return newDorm;
+ ), player.getId().toString(), newDorm.RoomId);
+
+ return await newDorm.export();
}
async generateNewDorm(player: Profile) {
const id = await this.#getAvailableRoomId();
- const basedorm: RoomDetails = {
- Room: {
- RoomId: id,
- Name: `DormRoom`,
- Description: "Your private room.",
- CreatorPlayerId: player.getId(),
- ImageName: "DefaultProfileImage.png",
- State: RoomState.Active,
- Accessibility: RoomAccessibility.Private,
- SupportsLevelVoting: false,
- IsAGRoom: false,
- IsDormRoom: true,
- CloningAllowed: false,
- SupportsScreens: true,
- SupportsTeleportVR: true,
- SupportsWalkVR: true,
- AllowsJuniors: true,
- RoomWarningMask: 0,
- CustomRoomWarning: "",
- DisableMicAutoMute: false
- },
- Scenes: [
- {
- RoomId: id,
- RoomSceneId: 1,
- Name: "Home",
- RoomSceneLocationId: "76d98498-60a1-430c-ab76-b54a29b7a163",
- IsSandbox: true,
- CanMatchmakeInto: true,
- MaxPlayers: 4,
- DataBlobName: "",
- DataModifiedAt: new Date().toISOString()
- }
- ],
- CoOwners: [],
- InvitedCoOwners: [],
- Hosts: [],
- InvitedHosts: [],
- CheerCount: 0,
- VisitCount: 0,
- FavoriteCount: 0,
- Tags: []
- }
- return basedorm;
+
+ const factory = await new RoomFactory({ id: id, factoryMode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree }).init();
+ if (!factory) return null;
+
+ factory.Name = "DormRoom";
+ factory.Description = "Your private room.";
+ factory.CreatorPlayerId = player.getId();
+ factory.ImageName = "DefaultProfileImage.png";
+ factory.State = RoomState.Active;
+ factory.RoomAccessibility = RoomAccessibility.Private;
+ factory.SupportsLevelVoting = false;
+ factory.IsAGRoom = false;
+ factory.IsDormRoom = true;
+ factory.CloningAllowed = false;
+ factory.AllowsJuniors = true;
+ factory.RoomWarningMask = 0;
+ factory.CustomRoomWarning = "";
+
+ factory.addHardwareSupport('*');
+
+ const subroomFactory = factory.getSubroom(await this.#getAvailableSubRoomId(id), FactoryMode.Write, WriteMode.WriteIfFree);
+ if (!subroomFactory) return null;
+
+ subroomFactory.RoomSceneLocationId = IntegratedRoomScene.DormRoom;
+ subroomFactory.Name = "Home";
+ subroomFactory.IsSandbox = true;
+ subroomFactory.DataBlobName = "";
+ subroomFactory.MaxPlayers = 4;
+ subroomFactory.CanMatchmakeInto = true;
+
+ factory.write();
+ subroomFactory.write();
+
+ return factory;
}
async generateBuiltinRooms() {
- if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.BuiltinGenerated))) !== null) return true;
- for (const builtinRoom of rooms) {
- if (builtinRoom.Name == 'DormRoom') continue;
+ if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.BuiltinGenerated))) !== null) return true;
+ await Promise.all(rooms.map(async builtinRoom => {
+ if (builtinRoom.Name == 'DormRoom') return;
const newId = await this.#getAvailableRoomId();
- await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.AGRooms), newId);
- const roomDets: RoomDetails = {
- Room: {
- Name: builtinRoom.Name,
- RoomId: newId,
- Description: builtinRoom.Description,
- CreatorPlayerId: 1,
- ImageName: `${builtinRoom.Name}.png`,
- State: RoomState.Active,
- Accessibility: builtinRoom.Accessibility,
- SupportsLevelVoting: builtinRoom.SupportsLevelVoting,
- IsAGRoom: true,
- IsDormRoom: builtinRoom.Name == 'DormRoom',
- CloningAllowed: builtinRoom.Name == 'DormRoom' ? true : builtinRoom.CloningAllowed,
- SupportsScreens: builtinRoom.SupportsScreens,
- SupportsWalkVR: builtinRoom.SupportsWalkVR,
- SupportsTeleportVR: builtinRoom.SupportsTeleportVR,
- AllowsJuniors: true,
- RoomWarningMask: 0,
- CustomRoomWarning: ""
- },
- Scenes: [],
- CoOwners: [],
- InvitedCoOwners: [],
- Hosts: [],
- InvitedHosts: [],
- CheerCount: 0,
- FavoriteCount: 0,
- VisitCount: 0,
- Tags: []
- }
- for (const subroom of builtinRoom.Scenes) {
- const newSubroomId = await this.#getAvailableSubRoomId(newId);
- roomDets.Scenes.push({
- RoomSceneId: newSubroomId,
- RoomId: newId,
- RoomSceneLocationId: subroom.RoomSceneLocationId,
- Name: subroom.Name,
- IsSandbox: subroom.IsSandbox,
- DataBlobName: "",
- MaxPlayers: subroom.MaxPlayers,
- CanMatchmakeInto: subroom.CanMatchmakeInto ? subroom.CanMatchmakeInto : true,
- DataModifiedAt: new Date().toISOString()
- });
- }
- await this.#setRoom(roomDets);
- }
- Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.BuiltinGenerated), "1");
- return false;
+ await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.AGRooms), newId);
+ const factory = await new RoomFactory({ id: newId, factoryMode: FactoryMode.Write, writeMode: WriteMode.Overwrite }).init();
+ if (!factory) return;
+
+ factory.Name = builtinRoom.Name;
+ factory.Description = builtinRoom.Description;
+ factory.CreatorPlayerId = 1;
+
+ const baseImageChanges = [
+ { room: "DodgeballVR", image: "Dodgeball" },
+ { room: "PaintballVR", image: "Paintball" },
+ { room: "StuntRunnerBaseRoom", image: "StuntRunner" },
+ { room: "BowlingAlley", image: "Bowling" },
+ ]
+
+ if (baseImageChanges.find(change => change.room == builtinRoom.Name)) {
+ const image = baseImageChanges.find(change => change.room == builtinRoom.Name)!;
+ factory.ImageName = `${image.image}.png`;
+ }
+ else factory.ImageName = `${builtinRoom.Name}.png`;
+
+ factory.State = RoomState.Active;
+ factory.RoomAccessibility = builtinRoom.Accessibility;
+ factory.SupportsLevelVoting = builtinRoom.SupportsLevelVoting;
+ factory.IsAGRoom = true;
+ factory.CloningAllowed = builtinRoom.CloningAllowed;
+ factory.AllowsJuniors = true;
+ factory.RoomWarningMask = 0;
+ factory.CustomRoomWarning = "";
+
+ if (builtinRoom.SupportsScreens) factory.addHardwareSupport('screens');
+ if (builtinRoom.SupportsTeleportVR) factory.addHardwareSupport('teleport_vr');
+ if (builtinRoom.SupportsWalkVR) factory.addHardwareSupport('walk_vr');
+
+ await Promise.all(builtinRoom.Scenes.map(async subroom => {
+ const newSubroomId = await this.#getAvailableSubRoomId(newId);
+ const subroomFactory = await factory.getSubroom(newSubroomId, FactoryMode.Write, WriteMode.Overwrite).init();
+ if (!subroomFactory) return;
+
+ subroomFactory.RoomSceneLocationId = subroom.RoomSceneLocationId;
+ subroomFactory.Name = subroom.Name;
+ subroomFactory.IsSandbox = subroom.IsSandbox;
+ subroomFactory.DataBlobName = "";
+ subroomFactory.MaxPlayers = subroom.MaxPlayers;
+ subroomFactory.CanMatchmakeInto = subroom.CanMatchmakeInto;
+
+ await subroomFactory.write();
+ }));
+
+ await factory.write();
+ }));
+ Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.BuiltinGenerated), "1");
+ return false;
}
async getIdFromName(name: string) {
@@ -344,23 +286,13 @@ class RoomsBase {
return parsedId;
}
- async getNameFromId(id: number) {
- const name = await Redis.Database.hget(Redis.buildKey(
- Redis.KeyGroups.Rooms.Root,
- id.toString(),
- this.roomRootKeys.Meta
- ), this.roomMetaKeys.Name);
- if (!name) return null;
- return name;
- }
-
async getSubroomIdsFromRoom(id: number): Promise;
async getSubroomIdsFromRoom(id: number, stringify: false): Promise;
async getSubroomIdsFromRoom(id: number, stringify: boolean | undefined = false): Promise {
const ids = await Redis.Database.smembers(Redis.buildKey(
Redis.KeyGroups.Rooms.Root,
id.toString(),
- this.roomRootKeys.Subrooms
+ RoomFactory.Keys.Subrooms
));
const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val));
diff --git a/src/data/content/roomtypes.ts b/src/data/content/rooms/DataTypes.ts
similarity index 84%
rename from src/data/content/roomtypes.ts
rename to src/data/content/rooms/DataTypes.ts
index 8a8bfe0..def392f 100644
--- a/src/data/content/roomtypes.ts
+++ b/src/data/content/rooms/DataTypes.ts
@@ -15,54 +15,19 @@ 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 . */
-export enum IntegratedRoomScene {
- Calibration = "f5fbd9c9-e853-4036-9d48-5f68e861af04",
- DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163",
- RecCenter = "cbad71af-0831-44d8-b8ef-69edafa841f6",
- ThreeDCharades = "4078dfed-24bb-4db7-863f-578ba48d726b",
- DiscGolfLake = "f6f7256c-e438-4299-b99e-d20bef8cf7e0",
- DiscGolfPropulsion = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55",
- Dodgeball = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
- Paddleball = "d89f74fa-d51e-477a-a425-025a891dd499",
- Paintball_River = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
- Paintball_Homestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
- Paintball_Quarry = "ff4c6427-7079-4f59-b22a-69b089420827",
- Paintball_Clearcut = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
- Paintball_Spillway = "58763055-2dfb-4814-80b8-16fac5c85709",
- PaintballVR_River = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
- PaintballVR_Homestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
- PaintballVR_Quarry = "ff4c6427-7079-4f59-b22a-69b089420827",
- PaintballVR_Clearcut = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
- PaintballVR_Spillway = "58763055-2dfb-4814-80b8-16fac5c85709",
- GoldenTrophy = "91e16e35-f48f-4700-ab8a-a1b79e50e51b",
- TheRiseofJumbotron = "acc06e66-c2d0-4361-b0cd-46246a4c455c",
- CrimsonCauldron = "949fa41f-4347-45c0-b7ac-489129174045",
- IsleOfLostSkulls = "7e01cfe0-820a-406f-b1b3-0a5bf575235c",
- Soccer = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03",
- LaserTagHangar = "239e676c-f12f-489f-bf3a-d4c383d692c3",
- LaserTagCyberJunk = "9d6456ce-6264-48b4-808d-2d96b3d91038",
- RecRoyaleSquads = "253fa009-6e65-4c90-91a1-7137a56a267f",
- RecRoyaleVR = "253fa009-6e65-4c90-91a1-7137a56a267f",
- RecRoyaleSolos = "b010171f-4875-4e89-baba-61e878cd41e1",
- Lounge = "a067557f-ca32-43e6-b6e5-daaec60b4f5a",
- PerformanceHall = "9932f88f-3929-43a0-a012-a40b5128e346",
- MakerRoom = "a75f7547-79eb-47c6-8986-6767abcb4f92",
- Park = "0a864c86-5a71-4e18-8041-8124e4dc9d98",
- ArtTesting = "42699ed2-0c1b-4f3d-93a2-ce01dfce7a79",
- River = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
- Homestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
- Quarry = "ff4c6427-7079-4f59-b22a-69b089420827",
- Clearcut = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
- Spillway = "58763055-2dfb-4814-80b8-16fac5c85709",
- Lake = "f6f7256c-e438-4299-b99e-d20bef8cf7e0",
- PropulsionTestRange = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55",
- Gym = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
- Stadium = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03",
- Hangar = "239e676c-f12f-489f-bf3a-d4c383d692c3",
- CyberJunkCity = "9d6456ce-6264-48b4-808d-2d96b3d91038",
- DodgeballVR = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
+export enum WriteMode {
+ Overwrite = "overwrite",
+ WriteIfFree = "if_free"
}
+export enum FactoryMode {
+ Fetch = 'fetch',
+ Write = 'write'
+}
+
+export type HardwareSupport = "screens" | "walk_vr" | "teleport_vr" | "low_vr" | "mobile";
+export const HardwareSupportStrings = ["screens", "walk_vr", "teleport_vr", "low_vr", "mobile"];
+
export enum RoomState {
Active,
PendingJunior = 11,
@@ -72,9 +37,9 @@ export enum RoomState {
}
export enum RoomAccessibility {
- Private,
- Public,
- Unlisted
+ Private,
+ Public,
+ Unlisted
}
export interface BuiltinScene {
@@ -131,7 +96,7 @@ export interface Room {
AllowsJuniors: boolean,
RoomWarningMask: number, // generated by dedicated mask generation function
CustomRoomWarning: string,
- DisableMicAutoMute?: boolean
+ DisableMicAutoMute?: boolean | null
}
export enum RoomWarningMask {
@@ -169,4 +134,58 @@ export interface RoomDetails {
FavoriteCount: number,
VisitCount: number,
Tags: TagDTO[]
-}
\ No newline at end of file
+}
+
+export enum IntegratedRoomScene {
+ DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163",
+ RecCenter = "cbad71af-0831-44d8-b8ef-69edafa841f6",
+ ThreeDCharades = "4078dfed-24bb-4db7-863f-578ba48d726b",
+ DiscGolfLake = "f6f7256c-e438-4299-b99e-d20bef8cf7e0",
+ DiscGolfPropulsion = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55",
+ Dodgeball = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
+ Paddleball = "d89f74fa-d51e-477a-a425-025a891dd499",
+ Paintball_River = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
+ Paintball_Homestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
+ Paintball_Quarry = "ff4c6427-7079-4f59-b22a-69b089420827",
+ Paintball_Clearcut = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
+ Paintball_Spillway = "58763055-2dfb-4814-80b8-16fac5c85709",
+ Paintball_DriveIn = "65ddbb48-5a01-4e3e-972d-e5c7419e2bc3",
+ PaintballVR_River = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
+ PaintballVR_Homestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
+ PaintballVR_Quarry = "ff4c6427-7079-4f59-b22a-69b089420827",
+ PaintballVR_Clearcut = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
+ PaintballVR_Spillway = "58763055-2dfb-4814-80b8-16fac5c85709",
+ GoldenTrophy = "91e16e35-f48f-4700-ab8a-a1b79e50e51b",
+ TheRiseofJumbotron = "acc06e66-c2d0-4361-b0cd-46246a4c455c",
+ CrimsonCauldron = "949fa41f-4347-45c0-b7ac-489129174045",
+ IsleOfLostSkulls = "7e01cfe0-820a-406f-b1b3-0a5bf575235c",
+ Soccer = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03",
+ LaserTag_Hangar = "239e676c-f12f-489f-bf3a-d4c383d692c3",
+ LaserTag_CyberJunkCity = "9d6456ce-6264-48b4-808d-2d96b3d91038",
+ RecRoyaleSquads = "253fa009-6e65-4c90-91a1-7137a56a267f",
+ RecRoyaleSolos = "b010171f-4875-4e89-baba-61e878cd41e1",
+ Lounge = "a067557f-ca32-43e6-b6e5-daaec60b4f5a",
+ PerformanceHall = "9932f88f-3929-43a0-a012-a40b5128e346",
+ MakerRoom = "a75f7547-79eb-47c6-8986-6767abcb4f92",
+ Park = "0a864c86-5a71-4e18-8041-8124e4dc9d98",
+ River = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
+ Homestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
+ Quarry = "ff4c6427-7079-4f59-b22a-69b089420827",
+ Clearcut = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
+ Spillway = "58763055-2dfb-4814-80b8-16fac5c85709",
+ Lake = "f6f7256c-e438-4299-b99e-d20bef8cf7e0",
+ PropulsionTestRange = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55",
+ Gym = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
+ Stadium = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03",
+ Hangar = "239e676c-f12f-489f-bf3a-d4c383d692c3",
+ CyberJunkCity = "9d6456ce-6264-48b4-808d-2d96b3d91038",
+ DodgeballVR = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
+ Crescendo = "49cb8993-a956-43e2-86f4-1318f279b22a",
+ Bowling = "ae929543-9a07-41d5-8ee9-dbbee8c36800",
+ BowlingAlley = "ae929543-9a07-41d5-8ee9-dbbee8c36800",
+ StuntRunner_StuntRunner = "b7281665-a715-4051-826b-8e08e69c6172",
+ StuntRunner_TheMainEvent = "3a636bd2-f896-424c-9225-c184522c0d87",
+ StuntRunnerBaseRoom = "882e9b96-7115-4b03-86f6-c0c9d8e22e00",
+}
+
+export * as RoomDataTypes from "./DataTypes.ts";
\ No newline at end of file
diff --git a/src/data/content/rooms/RoomFactory.ts b/src/data/content/rooms/RoomFactory.ts
new file mode 100644
index 0000000..cf9723a
--- /dev/null
+++ b/src/data/content/rooms/RoomFactory.ts
@@ -0,0 +1,359 @@
+/* Galvanic Corrosion - Rec Room custom server for communities.
+
+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 . */
+
+import { Redis } from "../../../db.ts";
+import Rooms from "../rooms.ts";
+import { FactoryMode, HardwareSupport, HardwareSupportStrings, RoomAccessibility, RoomDataTypes, RoomDetails, RoomState, WriteMode } from "./DataTypes.ts";
+import { SubroomFactory } from "./SubroomFactory.ts";
+
+interface RoomFactoryOptions {
+ id?: number;
+ name?: string;
+ writeMode?: RoomDataTypes.WriteMode;
+ factoryMode?: RoomDataTypes.FactoryMode;
+}
+
+export class RoomFactory {
+
+ static Keys = {
+ Meta: "roommeta",
+ Subrooms: "subrooms",
+ VisitCount: "visits",
+ HardwareSupport: "hardware"
+ }
+
+ #cannotFetchDormError = new Error("Cannot fetch the name 'DormRoom'");
+ #mustSpecifyEitherIdOrNameError = new Error("A room name or room ID must be specified");
+ #mustSpecifyIdInWriteModeError = new Error("A room ID must be specified in fetch mode");
+ #mustFetchRoomFirstError = new Error("Cannot get room data before fetching");
+ #cannotWriteInFetchModeError = new Error("Cannot write to database in fetch mode");
+ #cannotWriteToUnresolvedIdError = new Error("Cannot write to an unresolved room ID");
+ #roomAlreadyExistsError = new Error("Room already exists. Use overwrite mode to overwrite");
+ #hashValuesNotSetError = new Error("Room meta values not set");
+ #unresolvedError = new Error("Cannot get subroom of roomId that was not resolved. Did you call init()?");
+
+ #specifiedId?: number;
+ #specifiedName?: string;
+
+ #writeMode: RoomDataTypes.WriteMode = RoomDataTypes.WriteMode.WriteIfFree;
+ factoryMode: RoomDataTypes.FactoryMode = RoomDataTypes.FactoryMode.Fetch;
+
+ #resolvedId: number | null = null;
+
+ #hash: Record | null = null;
+
+ constructor(options: RoomFactoryOptions) {
+
+ if (options.name == 'DormRoom') throw this.#cannotFetchDormError;
+ this.#specifiedId = options.id;
+ this.#specifiedName = options.name;
+ if (options.writeMode) this.#writeMode = options.writeMode;
+ if (options.factoryMode) this.factoryMode = options.factoryMode;
+
+ }
+
+ async init() {
+
+ if (this.factoryMode !== FactoryMode.Fetch) {
+ if (!this.#specifiedId) throw this.#mustSpecifyIdInWriteModeError;
+ this.#resolvedId = this.#specifiedId;
+ return this;
+ }
+
+ if (!this.#specifiedId) {
+ if (!this.#specifiedName) throw this.#mustSpecifyEitherIdOrNameError;
+ const id = await Rooms.getIdFromName(this.#specifiedName);
+ if (!id) return null;
+ this.#specifiedId = id;
+ }
+
+ this.#resolvedId = this.#specifiedId;
+
+ this.#hash = await Redis.Database.hgetall(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#resolvedId.toString(),
+ RoomFactory.Keys.Meta
+ ));
+
+ return this;
+
+ }
+
+ async write() {
+
+ const id = this.#resolvedId;
+ if (!id) throw this.#cannotWriteToUnresolvedIdError;
+
+ if (this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#cannotWriteInFetchModeError;
+ else {
+
+ const dbkey = Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ id.toString(),
+ RoomFactory.Keys.Meta
+ );
+
+ if (this.#writeMode == RoomDataTypes.WriteMode.WriteIfFree) {
+ const exists = await Redis.Database.exists(dbkey);
+ const nameExists = this.Name == 'DormRoom' ? 0 : await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Room_Names, this.Name));
+ if (exists >= 1 || nameExists >= 1) throw this.#roomAlreadyExistsError;
+ }
+
+ if (!this.#hash) throw this.#hashValuesNotSetError;
+ await Redis.Database.hset(dbkey, this.#hash);
+
+ }
+
+ if (this.Name !== 'DormRoom') await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Room_Names, this.Name), this.RoomId);
+ }
+
+ async export() {
+ const hardwareSupport = await this.getHardwareSupport();
+ const subroomIds = await this.getAllSubroomIds();
+
+ const subroomPromises = subroomIds.map(id => this.getSubroom(id).init());
+ const subrooms = (await Promise.all(subroomPromises)).map(subroom => subroom.export());
+
+ const details: RoomDetails = {
+ Room: {
+ RoomId: this.RoomId,
+ Name: this.Name,
+ Description: this.Description,
+ CreatorPlayerId: this.CreatorPlayerId,
+ ImageName: this.ImageName,
+ State: this.State,
+ Accessibility: this.RoomAccessibility,
+ SupportsLevelVoting: this.SupportsLevelVoting,
+ IsAGRoom: this.IsAGRoom,
+ IsDormRoom: this.IsDormRoom ? true : undefined,
+ CloningAllowed: this.CloningAllowed,
+ SupportsScreens: hardwareSupport.includes('screens'),
+ SupportsWalkVR: hardwareSupport.includes('walk_vr'),
+ SupportsTeleportVR: hardwareSupport.includes('teleport_vr'),
+ AllowsJuniors: this.AllowsJuniors,
+ RoomWarningMask: this.RoomWarningMask,
+ CustomRoomWarning: this.CustomRoomWarning,
+ DisableMicAutoMute: this.DisableMicAutoMute ? true : undefined
+ },
+ Scenes: subrooms,
+ CoOwners: [],
+ InvitedCoOwners: [],
+ Hosts: [],
+ InvitedHosts: [],
+ CheerCount: 0,
+ FavoriteCount: 0,
+ VisitCount: await this.getVisitCount(),
+ Tags: []
+ }
+
+ return details;
+ }
+
+ getSubroom(id: number, factoryMode?: FactoryMode, writeMode?: WriteMode) {
+ if (!this.#resolvedId) throw this.#unresolvedError;
+ return new SubroomFactory({
+ roomId: this.#resolvedId,
+ subroomId: id,
+ factoryMode: factoryMode ? factoryMode : undefined,
+ writeMode : writeMode ? writeMode : undefined
+ });
+ }
+
+ async getAllSubroomIds() {
+ if (!this.#resolvedId) throw this.#unresolvedError;
+
+ return (await Redis.Database.smembers(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#resolvedId.toString(),
+ RoomFactory.Keys.Subrooms
+ ))).map(val => parseInt(val)).filter(val => !isNaN(val));
+ }
+
+ #fetchStringKey(key: string, def: string) {
+ if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchRoomFirstError;
+ else if (!this.#hash) return def;
+ return this.#hash[key];
+ }
+
+ #fetchNumberKey(key: string, def: number) {
+ if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchRoomFirstError;
+ else if (!this.#hash) return def;
+ const val = this.#hash[key];
+ if (isNaN(parseInt(val))) return def;
+ else return parseInt(val);
+ }
+
+ #fetchBooleanKey(key: string, def: boolean) {
+ if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchRoomFirstError;
+ else if (!this.#hash) return def;
+ const val = this.#hash[key];
+ try {
+ return JSON.parse(val) as boolean;
+ } catch {
+ return def;
+ }
+ }
+
+ #setHashValue(key: string, value: string | number | boolean) {
+ if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchRoomFirstError;
+
+ if (!this.#hash) this.#hash = {};
+
+ if (typeof value === 'object' && value !== null) {
+ const val = JSON.stringify(value);
+ if (!val) throw new Error("Cannot stringify given value");
+
+ this.#hash[key] = val;
+ } else this.#hash[key] = value.toString();
+ }
+
+ get RoomId() { if (!this.#resolvedId) throw this.#unresolvedError; return this.#resolvedId; }
+
+ #nameKey = 'Name';
+ get Name() { return this.#fetchStringKey(this.#nameKey, 'DATABASEERROR') }
+ set Name(data) { this.#setHashValue(this.#nameKey, data) }
+
+ #descKey = 'Description';
+ get Description() { return this.#fetchStringKey(this.#descKey, 'Database Error. Contact an administrator.') }
+ set Description(data) { this.#setHashValue(this.#descKey, data) }
+
+ #creatorKey = 'CreatorPlayerId';
+ get CreatorPlayerId() { return this.#fetchNumberKey(this.#creatorKey, 1) }
+ set CreatorPlayerId(data) { this.#setHashValue(this.#creatorKey, data) }
+
+ #imageKey = 'ImageName';
+ get ImageName() { return this.#fetchStringKey(this.#imageKey, 'DefaultProfileImage.png') }
+ set ImageName(data) { this.#setHashValue(this.#imageKey, data) }
+
+ #stateKey = 'State';
+ get State(): RoomState { return this.#fetchNumberKey(this.#stateKey, RoomState.Active) }
+ set State(data) { this.#setHashValue(this.#stateKey, data) }
+
+ #accessKey = 'RoomAccessibility';
+ get RoomAccessibility(): RoomAccessibility { return this.#fetchNumberKey(this.#accessKey, RoomAccessibility.Unlisted) }
+ set RoomAccessibility(data) { this.#setHashValue(this.#accessKey, data) }
+
+ #votingKey = 'SupportsLevelVoting';
+ get SupportsLevelVoting() { return this.#fetchBooleanKey(this.#votingKey, false) }
+ set SupportsLevelVoting(data) { this.#setHashValue(this.#votingKey, data) }
+
+ #agroomKey = 'IsAGRoom';
+ get IsAGRoom() { return this.#fetchBooleanKey(this.#agroomKey, true) }
+ set IsAGRoom(data) { this.#setHashValue(this.#agroomKey, data) }
+
+ #dormKey = 'IsDormRoom';
+ get IsDormRoom() { return this.#fetchBooleanKey(this.#dormKey, false) }
+ set IsDormRoom(data) { this.#setHashValue(this.#dormKey, data) }
+
+ #cloningKey = 'CloningAllowed';
+ get CloningAllowed() { return this.#fetchBooleanKey(this.#cloningKey, false) }
+ set CloningAllowed(data) { this.#setHashValue(this.#cloningKey, data) }
+
+ #juniorsKey = 'AllowsJuniors';
+ get AllowsJuniors() { return this.#fetchBooleanKey(this.#juniorsKey, false) }
+ set AllowsJuniors(data) { this.#setHashValue(this.#juniorsKey, data) }
+
+ #maskKey = 'RoomWarningMask';
+ get RoomWarningMask() { return this.#fetchNumberKey(this.#maskKey, 0) }
+ set RoomWarningMask(data) { this.#setHashValue(this.#maskKey, data) }
+
+ #warnKey = 'CustomRoomWarning';
+ get CustomRoomWarning() { return this.#fetchStringKey(this.#warnKey, '') }
+ set CustomRoomWarning(data) { this.#setHashValue(this.#warnKey, data) }
+
+ #muteKey = 'DisableMicAutoMute';
+ get DisableMicAutoMute() { return this.#fetchBooleanKey(this.#muteKey, false) }
+ set DisableMicAutoMute(data) { this.#setHashValue(this.#muteKey, data) }
+
+ async getHardwareSupport(): Promise {
+ if (!this.#resolvedId) throw this.#unresolvedError;
+ return (await Redis.Database.smembers(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#resolvedId.toString(),
+ RoomFactory.Keys.HardwareSupport
+ ))) as HardwareSupport[];
+ }
+
+ async addHardwareSupport(hardware: HardwareSupport | HardwareSupport[] | '*') {
+ if (!this.#resolvedId) throw this.#unresolvedError;
+
+ if (hardware === '*') {
+ await Promise.all(HardwareSupportStrings.map(str => this.addHardwareSupport(str as HardwareSupport) ));
+ return;
+ }
+
+ if (typeof hardware == 'string') {
+ await Redis.Database.sadd(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#resolvedId.toString(),
+ RoomFactory.Keys.HardwareSupport
+ ), hardware);
+ } else {
+
+ const promises = hardware.map(hw => Redis.Database.sadd(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#resolvedId!.toString(),
+ RoomFactory.Keys.HardwareSupport
+ ), hw));
+ await Promise.all(promises);
+
+ }
+ }
+
+ async removeHardwareSupport(hardware: HardwareSupport) {
+ if (!this.#resolvedId) throw this.#unresolvedError;
+ await Redis.Database.srem(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#resolvedId.toString(),
+ RoomFactory.Keys.HardwareSupport
+ ), hardware);
+ }
+
+ async getVisitCount() {
+ if (!this.#resolvedId) throw this.#unresolvedError;
+
+ const visits = await Redis.Database.get(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#resolvedId.toString(),
+ RoomFactory.Keys.VisitCount
+ ));
+ if (!visits) return 0;
+ const parsedVisits = parseInt(visits);
+ if (isNaN(parsedVisits)) return 0;
+ else return parsedVisits;
+ }
+
+ async addVisit() {
+ if (!this.#resolvedId) throw this.#unresolvedError;
+
+ let visits = await Redis.Database.get(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#resolvedId.toString(),
+ RoomFactory.Keys.VisitCount
+ ));
+ if (!visits) visits = "0";
+ let parsedVisits = parseInt(visits);
+ if (isNaN(parsedVisits)) parsedVisits = 0;
+
+ await Redis.Database.set(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#resolvedId.toString(),
+ RoomFactory.Keys.VisitCount
+ ), parsedVisits + 1);
+ }
+
+}
\ No newline at end of file
diff --git a/src/data/content/rooms/SubroomFactory.ts b/src/data/content/rooms/SubroomFactory.ts
new file mode 100644
index 0000000..d739911
--- /dev/null
+++ b/src/data/content/rooms/SubroomFactory.ts
@@ -0,0 +1,227 @@
+/* Galvanic Corrosion - Rec Room custom server for communities.
+
+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 . */
+
+import { Redis } from "../../../db.ts";
+import { RoomDataTypes, IntegratedRoomScene, RoomScene, WriteMode, FactoryMode } from "./DataTypes.ts";
+import { RoomFactory } from "./RoomFactory.ts";
+
+interface SubroomFactoryOptions {
+ roomId: number;
+ subroomId: number;
+ writeMode?: WriteMode;
+ factoryMode?: FactoryMode;
+}
+
+export class SubroomFactory {
+
+ static Keys = {
+ Meta: "meta",
+ Blobs: "blobs"
+ }
+
+ #mustFetchSubroomFirstError = new Error("Cannot get subroom data before fetching");
+ #unspecifiedArguments = new Error("A roomId and subroomId must be specified");
+ #cannotWriteInFetchModeError = new Error("Cannot write to database in fetch mode");
+ #subroomAlreadyExistsError = new Error("Subroom already exists. Use overwrite mode to overwrite");
+ #hashValuesNotSetError = new Error("Subroom meta values not set");
+
+ #writeMode: RoomDataTypes.WriteMode = RoomDataTypes.WriteMode.WriteIfFree;
+ factoryMode: RoomDataTypes.FactoryMode = RoomDataTypes.FactoryMode.Fetch;
+
+ #roomId: number;
+ #subroomId: number;
+
+ #hash: Record | null = null;
+
+ constructor(options: SubroomFactoryOptions) {
+ this.#roomId = options.roomId;
+ this.#subroomId = options.subroomId;
+ if (options.writeMode) this.#writeMode = options.writeMode;
+ if (options.factoryMode) this.factoryMode = options.factoryMode;
+ }
+
+ async init() {
+
+ if (!this.#roomId || !this.#subroomId) throw this.#unspecifiedArguments;
+
+ this.#hash = await Redis.Database.hgetall(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#roomId.toString(),
+ RoomFactory.Keys.Subrooms,
+ this.#subroomId.toString(),
+ SubroomFactory.Keys.Meta
+ ));
+
+ return this;
+
+ }
+
+ async write() {
+
+ if (this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#cannotWriteInFetchModeError;
+ else {
+
+ const dbkey = Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#roomId.toString(),
+ RoomFactory.Keys.Subrooms,
+ this.#subroomId.toString(),
+ SubroomFactory.Keys.Meta
+ );
+
+ if (this.#writeMode == RoomDataTypes.WriteMode.WriteIfFree) {
+ const exists = await Redis.Database.exists(dbkey);
+ if (exists >= 1) throw this.#subroomAlreadyExistsError;
+ }
+
+ if (!this.#hash) throw this.#hashValuesNotSetError;
+ this.#hash['DataModifiedAt'] = new Date().toISOString();
+
+ await Redis.Database.hset(dbkey, this.#hash);
+
+ }
+
+ await Redis.Database.sadd(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#roomId.toString(),
+ RoomFactory.Keys.Subrooms
+ ), this.RoomSceneId);
+
+ }
+
+ export(): RoomScene {
+ return {
+ RoomSceneId: this.RoomSceneId,
+ RoomId: this.RoomId,
+ RoomSceneLocationId: this.RoomSceneLocationId,
+ Name: this.Name,
+ IsSandbox: this.IsSandbox,
+ DataBlobName: this.DataBlobName,
+ MaxPlayers: this.MaxPlayers,
+ CanMatchmakeInto: this.CanMatchmakeInto,
+ DataModifiedAt: this.DataModifiedAt
+ };
+ }
+
+ #fetchStringKey(key: string, def: string) {
+ if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchSubroomFirstError;
+ else if (!this.#hash) return def;
+ return this.#hash[key];
+ }
+
+ #fetchNumberKey(key: string, def: number) {
+ if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchSubroomFirstError;
+ else if (!this.#hash) return def;
+ const val = this.#hash[key];
+ if (isNaN(parseInt(val))) return def;
+ else return parseInt(val);
+ }
+
+ #fetchBooleanKey(key: string, def: boolean) {
+ if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchSubroomFirstError;
+ else if (!this.#hash) return def;
+ const val = this.#hash[key];
+ try {
+ return JSON.parse(val) as boolean;
+ } catch {
+ return def;
+ }
+ }
+
+ #setHashValue(key: string, value: string | number | boolean) {
+ if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchSubroomFirstError;
+
+ if (!this.#hash) this.#hash = {};
+
+ if (typeof value === 'object' && value !== null) {
+ const val = JSON.stringify(value);
+ if (!val) throw new Error("Cannot stringify given value");
+
+ this.#hash[key] = val;
+ this.#hash[this.#modifiedKey] = new Date().toISOString();
+ } else this.#hash[key] = value.toString();
+ }
+
+ get RoomSceneId() { return this.#subroomId }
+
+ get RoomId() { return this.#roomId }
+
+ #locationKey = 'RoomSceneLocationId';
+ get RoomSceneLocationId(): IntegratedRoomScene { return this.#fetchStringKey(this.#locationKey, IntegratedRoomScene.PerformanceHall) as IntegratedRoomScene }
+ set RoomSceneLocationId(data) { this.#setHashValue(this.#locationKey, data) }
+
+ #nameKey = 'Name';
+ get Name() { return this.#fetchStringKey(this.#nameKey, "Home") }
+ set Name(data) { this.#setHashValue(this.#nameKey, data) }
+
+ #sandboxKey = 'IsSandbox';
+ get IsSandbox() { return this.#fetchBooleanKey(this.#sandboxKey, false) }
+ set IsSandbox(data) { this.#setHashValue(this.#sandboxKey, data) }
+
+ #blobKey = 'DataBlobName';
+ get DataBlobName() { return this.#fetchStringKey(this.#blobKey, "") }
+ set DataBlobName(data) { this.#setHashValue(this.#blobKey, data) }
+
+ #playersKey = 'MaxPlayers';
+ get MaxPlayers() { return this.#fetchNumberKey(this.#playersKey, 8) }
+ set MaxPlayers(data) { this.#setHashValue(this.#playersKey, data) }
+
+ #matchKey = 'CanMatchmakeInto';
+ get CanMatchmakeInto() { return this.#fetchBooleanKey(this.#matchKey, true) }
+ set CanMatchmakeInto(data) { this.#setHashValue(this.#matchKey, data) }
+
+ #modifiedKey = 'DataModifiedAt';
+ get DataModifiedAt() { return this.#fetchStringKey(this.#modifiedKey, new Date().toISOString()) }
+
+ async addBlobHistory(date: Date, filename: string) {
+ await Redis.Database.hset(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#roomId.toString(),
+ RoomFactory.Keys.Subrooms,
+ this.#subroomId.toString(),
+ SubroomFactory.Keys.Blobs
+ ), date.toISOString(), filename);
+ }
+
+ async getBlobHistory() {
+ interface History {
+ time: Date,
+ filename: string
+ }
+
+ const hist = await Redis.Database.hgetall(Redis.buildKey(
+ Redis.KeyGroups.Rooms.Root,
+ this.#roomId.toString(),
+ RoomFactory.Keys.Subrooms,
+ this.#subroomId.toString(),
+ SubroomFactory.Keys.Blobs
+ ));
+
+ return Object.keys(hist).map(key => {
+ const d = new Date(key);
+ if (d instanceof Date && !isNaN(d.getTime())) return null;
+ else {
+ const h: History = {
+ time: d,
+ filename: hist[key]
+ }
+ return h;
+ }
+ }).filter(val => val !== null);
+ }
+
+}
\ No newline at end of file
diff --git a/src/data/live/base.ts b/src/data/live/base.ts
index 6819e0a..20e162d 100644
--- a/src/data/live/base.ts
+++ b/src/data/live/base.ts
@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
import Rooms from "../content/rooms.ts";
-import { RoomAccessibility, RoomState } from "../content/roomtypes.ts";
+import { RoomDataTypes } from "../content/rooms/DataTypes.ts";
import { Profile } from "../profiles.ts";
import Instances from "./instances.ts";
import { MatchmakingErrorCode, RoomInstance } from "./types.ts";
@@ -77,9 +77,9 @@ class MatchmakingBase {
// check to make sure room exists, is not private, and is active
const targetRoom = options.roomName !== 'DormRoom' ? await Rooms.getByName(options.roomName) : await Rooms.getProfileDormDefault(options.profile);
if (!targetRoom) return { errorCode: MatchmakingErrorCode.NoSuchRoom };
- if (targetRoom.Room.Accessibility == RoomAccessibility.Private && targetRoom.Room.CreatorPlayerId !== options.profile.getId())
+ if (targetRoom.Room.Accessibility == RoomDataTypes.RoomAccessibility.Private && targetRoom.Room.CreatorPlayerId !== options.profile.getId())
return { errorCode: MatchmakingErrorCode.RoomIsPrivate };
- if (targetRoom.Room.State !== RoomState.Active) return { errorCode: MatchmakingErrorCode.RoomIsNotActive };
+ if (targetRoom.Room.State !== RoomDataTypes.RoomState.Active) return { errorCode: MatchmakingErrorCode.RoomIsNotActive };
const roomId = targetRoom.Room.RoomId;
Instances.clearAllRoomEmptyInstances(roomId);
diff --git a/src/data/live/types.ts b/src/data/live/types.ts
index 58c8646..60bb408 100644
--- a/src/data/live/types.ts
+++ b/src/data/live/types.ts
@@ -15,7 +15,7 @@ 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 . */
-import { RoomDetails } from "../content/roomtypes.ts";
+import { RoomDataTypes } from "../content/rooms/DataTypes.ts";
import { Profile } from "../profiles.ts";
export enum PhotonRegionCodeString {
@@ -70,7 +70,7 @@ export interface RoomInstance {
export interface InstanceOptions {
- Room: RoomDetails,
+ Room: RoomDataTypes.RoomDetails,
SceneIndex: number,
EventId?: number,
Name?: string,
diff --git a/src/data/profile/progression.ts b/src/data/profile/progression.ts
index 0616a51..87062b5 100644
--- a/src/data/profile/progression.ts
+++ b/src/data/profile/progression.ts
@@ -24,8 +24,24 @@ const log = new Logging("ProfileProgression");
const config = GameConfigs.getConfig();
+interface PlayerProgressionExport {
+ PlayerId: number,
+ Level: number,
+ XP: number
+}
+
export class ProfileProgressionManager extends ProfileContentManager {
+ async export() {
+ const ex: PlayerProgressionExport = {
+ PlayerId: this.profileId,
+ Level: await this.getLevel(),
+ XP: await this.getXp()
+ }
+
+ return ex;
+ }
+
/**
* Set the profile's exact # of XP
* @returns The new # of XP
@@ -54,7 +70,7 @@ export class ProfileProgressionManager extends ProfileContentManager {
const xp = await this.getXp();
if (xp == null) return 1; // fallback since progression data is required
- if (typeof config?.LevelProgressionMaps == 'undefined') return null;
+ if (typeof config?.LevelProgressionMaps == 'undefined') return 1;
for (const item of config?.LevelProgressionMaps) {
if (xp >= item.RequiredXp) {
@@ -64,6 +80,8 @@ export class ProfileProgressionManager extends ProfileContentManager {
}
}
+
+ return 1; // fallback
}
async getXp() {
diff --git a/src/data/profile/reputation.ts b/src/data/profile/reputation.ts
index d9ad485..46d4a1d 100644
--- a/src/data/profile/reputation.ts
+++ b/src/data/profile/reputation.ts
@@ -31,7 +31,7 @@ export class ProfileReputationManager extends ProfileContentManager {
CheerCredit: 0,
SubscriberCount: 0,
SubscribedCount: 0,
- SelectedCheer: 0
+ SelectedCheer: null
};
}
diff --git a/src/data/profile/settings.ts b/src/data/profile/settings.ts
index 0c5777a..5830979 100644
--- a/src/data/profile/settings.ts
+++ b/src/data/profile/settings.ts
@@ -28,10 +28,6 @@ export class ProfileSettingsManager extends ProfileContentManager {
override async onProfileInit() {
await this.setSetting(SettingKey.RecroomOOBE, SettingDefault.RecroomOOBE);
- await this.setSetting(SettingKey.PlayerStatusVisibility, SettingDefault.PlayerStatusVisibility);
- await this.setSetting(SettingKey.FIRST_TIME_IN_FLAGS, SettingDefault.FIRST_TIME_IN_FLAGS);
- await this.setSetting(SettingKey.BACKPACK_FAVORITE_TOOL, SettingDefault.BACKPACK_FAVORITE_TOOL);
- await this.setSetting(SettingKey.Recroom_ChallengeMap, SettingDefault.Recroom_ChallengeMap);
}
async getSettings() {
diff --git a/src/data/usernames.ts b/src/data/usernames.ts
index cc66a3b..1284a42 100644
--- a/src/data/usernames.ts
+++ b/src/data/usernames.ts
@@ -15,242 +15,27 @@ 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://randomusernameapi.github.io/
const Dictionary = {
Adjectives: [
- "Amazing",
- "Affable",
- "Agreeable",
- "Ambitious",
- "Amicable",
- "Animated",
- "Astute",
- "Authentic",
- "Blissful",
- "Bold",
- "Bright",
- "Buoyant",
- "Calm",
- "Cheerful",
- "Clever",
- "Confident",
- "Content",
- "Creative",
- "Cultured",
- "Curious",
- "Dashing",
- "Dazzling",
- "Dedicated",
- "Diligent",
- "Dynamic",
- "Earnest",
- "Easygoing",
- "Ebullient",
- "Endearing",
- "Energetic",
- "Engaging",
- "Exuberant",
- "Fantastic",
- "Fearless",
- "Fervent",
- "Friendly",
- "Funny",
- "Generous",
- "Gentle",
- "Genuine",
- "Gracious",
- "Grateful",
- "Helpful",
- "Honest",
- "Humble",
- "Humorous",
- "Incisive",
- "Ingenious",
- "Intuitive",
- "Jovial",
- "Jubilant",
- "Just",
- "Kind",
- "Likable",
- "Lively",
- "Lovable",
- "Loving",
- "Loyal",
- "Luminous",
- "Magnetic",
- "Marvelous",
- "Masterful",
- "Mature",
- "Merciful",
- "Mindful",
- "Motivated",
- "Natural",
- "Nurturing",
- "Observant",
- "Outgoing",
- "Patient",
- "Peaceful",
- "Placid",
- "Playful",
- "Pleasant",
- "Poised",
- "Positive",
- "Powerful",
- "Pragmatic",
- "Proactive",
- "Prudent",
- "Punctual",
- "Radiant",
- "Rational",
- "Real",
- "Receptive",
- "Reliable",
- "Resilient",
- "Robust",
- "Sagacious",
- "Serene",
- "Sincere",
- "Skillful",
- "Smart",
- "Sociable",
- "Spirited",
- "Splendid",
- "Steady",
- "Sterling",
- "Strong",
- "Sublime",
- "Talented",
- "Tenacious",
- "Tireless",
- "Tolerant",
- "Tough",
- "Tranquil",
- "Unique",
- "Upbeat",
- "Valiant",
- "Vibrant",
- "Virtuous",
- "Visionary",
- "Vivacious",
- "Welcoming",
- "Wise",
- "Witty",
- "Wonderful",
- "Zealous",
+ "Alpha", "Zen", "Ruby", "Pixel", "Captain",
+ "Luna", "Quantum", "Emerald", "Serene", "Sushi",
+ "Mountain", "Phoenix", "Electric", "Songbird", "Tech",
+ "Silver", "Midnight", "Tango", "Cosmic", "Jazz",
+ "Velvet", "Neon", "Ghostly", "Ballet", "Delta",
+ "Echo", "Solar", "Pirate", "Harmonic",
+ "Cyber", "Melody", "Quasar", "Crimson", "Enigma",
+ "Stardust", "Techno", "Lunar", "Rogue", "Dream"
],
Nouns: [
- "Nomad",
- "Solstice",
- "Elysium",
- "Horizon",
- "Catalyst",
- "Utopia",
- "Eclipse",
- "Nebula",
- "Arcadia",
- "Apex",
- "Harmony",
- "Zenith",
- "Radiant",
- "Infinity",
- "Echo",
- "Quasar",
- "Cascade",
- "Empyrean",
- "Nebula",
- "Odyssey",
- "Aether",
- "Empower",
- "Zephyr",
- "Vibrance",
- "Astral",
- "Jubilant",
- "Zen",
- "Nebulous",
- "Ecliptic",
- "Stellar",
- "Quantum",
- "Ethereal",
- "Nexus",
- "Synergy",
- "Quantum",
- "Enigma",
- "Luminous",
- "Epoch",
- "Zenithal",
- "Paragon",
- "Panorama",
- "Maverick",
- "Voyager",
- "Luminary",
- "Catalyst",
- "Phoenix",
- "Dynamo",
- "Zenith",
- "Nexus",
- "Pinnacle",
- "Rhapsody",
- "Serenity",
- "Quantum",
- "Apex",
- "Harmony",
- "Odyssey",
- "Endeavor",
- "Visionary",
- "Epoch",
- "Panache",
- "Jubilee",
- "Resonance",
- "Zen",
- "Nimbus",
- "Ethereal",
- "Cascade",
- "Radiance",
- "Nebula",
- "Equinox",
- "Pulsar",
- "Apex",
- "Ethos",
- "Zenith",
- "Nebula",
- "Vertex",
- "Equinox",
- "Odyssey",
- "Pantheon",
- "Elysian",
- "Nebulous",
- "Quantum",
- "Harmonic",
- "Luminance",
- "Paragon",
- "Radiant",
- "Epoch",
- "Vortex",
- "Celestia",
- "Infinitum",
- "Empyrean",
- "Zephyr",
- "Nimbus",
- "Seraphic",
- "Enigma",
- "Synergy",
- "Ecliptic",
- "Utopian",
- "Phoenix",
- "Catalyst",
- "Euphoria",
- "Astral",
- "Nebula",
- "Ethereal",
- "Zenith",
- "Nexus",
- "Empower",
- "Panorama",
- "Cascade",
- "Quantum",
- "Jubilant",
- "Zen",
- "Radiance",
- "Labyrinth",
+ "Wolf", "Master", "Red", "Pirate", "Adventure",
+ "Lovegood", "Coder", "Enigma", "Seeker", "Samurai",
+ "Mover", "Fire", "Echo", "Soul", "Titan",
+ "Shadow", "Mystic", "Tornado", "Crafter", "Journey",
+ "Vortex", "Nebula", "Gazer", "Blossom", "Dynamo",
+ "Eagle", "Symphony", "Willow", "Pioneer", "Hawk",
+ "Scribe", "Mistress", "Quest", "Comet", "Explorer",
+ "Strider", "Trance", "Lullaby", "Dancer"
],
};
diff --git a/src/main.ts b/src/main.ts
index 7da36d9..bd7af02 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -130,7 +130,7 @@ try {
const authHeader = req.headers.get('authorization');
if (!authHeader) return { valid: false } as AuthResult;
- log.d(authHeader);
+ //log.d(authHeader);
const token = authHeader.split(", ")[1]; // Why is the header formatted like this?
if (!token) return { valid: false } as AuthResult;
const splitToken = token.split(' ')[1];
@@ -220,6 +220,7 @@ try {
if (!(await GameConfigs.getGameConfig('splitTestSoftOverrides'))) GameConfigs.setGameConfig('splitTestSoftOverrides', '');
if (!(await GameConfigs.getGameConfig('splitTestHardOverrides'))) GameConfigs.setGameConfig('splitTestHardOverrides', '');
+ log.i('Startup done.');
});
http.on('error', err => {
diff --git a/src/routes/api.ts b/src/routes/api.ts
index 3cd01dc..afb4d09 100644
--- a/src/routes/api.ts
+++ b/src/routes/api.ts
@@ -38,6 +38,7 @@ import { route as ImagesRoute } from "./api/images.ts";
import { route as CommunityBoardRoute } from "./api/communityboard.ts";
import { route as PlayerEventsRoute } from "./api/playerevents.ts";
import { route as StorefrontsRoute } from "./api/storefronts.ts";
+import { route as AnnouncementRoute } from "./api/announcement.ts";
export const route = APIUtils.createRouter("/api");
@@ -62,4 +63,5 @@ route.router.use(ChecklistRoute.path, ChecklistRoute.router);
route.router.use(ImagesRoute.path, ImagesRoute.router);
route.router.use(CommunityBoardRoute.path, CommunityBoardRoute.router);
route.router.use(PlayerEventsRoute.path, PlayerEventsRoute.router);
-route.router.use(StorefrontsRoute.path, StorefrontsRoute.router);
\ No newline at end of file
+route.router.use(StorefrontsRoute.path, StorefrontsRoute.router);
+route.router.use(AnnouncementRoute.path, AnnouncementRoute.router);
\ No newline at end of file
diff --git a/src/routes/api/announcement.ts b/src/routes/api/announcement.ts
new file mode 100644
index 0000000..30d49ad
--- /dev/null
+++ b/src/routes/api/announcement.ts
@@ -0,0 +1,30 @@
+/* Galvanic Corrosion - Rec Room custom server for communities.
+
+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 . */
+
+import { APIUtils } from "../../apiutils.ts";
+import { AuthType } from "../../data/users.ts";
+
+export const route = APIUtils.createRouter("/announcement");
+
+route.router.get('/v1/get',
+
+ APIUtils.Authentication,
+ APIUtils.AuthenticationType(AuthType.Game),
+
+ APIUtils.emptyArrayResponse
+
+);
\ No newline at end of file
diff --git a/src/routes/api/playerReputation.ts b/src/routes/api/playerReputation.ts
index d6f461a..fcedc1b 100644
--- a/src/routes/api/playerReputation.ts
+++ b/src/routes/api/playerReputation.ts
@@ -15,7 +15,8 @@ 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 . */
-import { APIUtils } from "../../apiutils.ts";
+import { z } from "zod";
+import { APIUtils, NoBody } from "../../apiutils.ts";
import UnifiedProfile from "../../data/profiles.ts";
import { AuthType } from "../../data/users.ts";
import express from "express";
@@ -40,14 +41,36 @@ route.router.get('/v1/:id',
);
-route.router.get('/v1/bulk',
+interface ReputationBulkBody {
+ Ids: string[] | string
+}
+const reputationBulkSchema = z.object({
+ Ids: z.union([
+ z.array(z.string()),
+ z.string()
+ ])
+});
+route.router.post('/v1/bulk',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
-
express.urlencoded({ extended: true }),
- APIUtils.logBody,
+ APIUtils.validateRequestBody(reputationBulkSchema),
- APIUtils.emptyArrayResponse
+ async (rq: express.Request, rs: express.Response) => {
+ if (typeof rq.body.Ids == 'object') {
+ const reputations = rq.body.Ids
+ .map(id => parseInt(id)).filter(id => !isNaN(id)) // parse as int[] and filter out non-numbers
+ .map(id => UnifiedProfile.get(id).Reputation.getReputation()); // get all reputations
+ rs.json(await Promise.all(reputations));
+ } else {
+ const id = parseInt(rq.body.Ids);
+ if (isNaN(id)) {
+ rs.sendStatus(400);
+ return;
+ }
+ rs.json([await UnifiedProfile.get(id).Reputation.getReputation()]);
+ }
+ },
);
\ No newline at end of file
diff --git a/src/routes/api/players.ts b/src/routes/api/players.ts
index 18add56..ecb7328 100644
--- a/src/routes/api/players.ts
+++ b/src/routes/api/players.ts
@@ -16,10 +16,11 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
import Logging from "@proxnet/undead-logging";
-import { APIUtils } from "../../apiutils.ts";
+import { APIUtils, NoBody } from "../../apiutils.ts";
import express from "express";
import UnifiedProfile from "../../data/profiles.ts";
import { AuthType } from "../../data/users.ts";
+import { z } from "zod";
const log = new Logging("ProgressionRoute");
@@ -51,13 +52,36 @@ route.router.get('/v1/progression/:id',
);
+interface ProgressionBulkBody {
+ Ids: string[] | string
+}
+const progressionBulkSchema = z.object({
+ Ids: z.union([
+ z.array(z.string()),
+ z.string()
+ ])
+});
route.router.post('/v1/progression/bulk',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
express.urlencoded({ extended: true }),
- APIUtils.logBody,
+ APIUtils.validateRequestBody(progressionBulkSchema),
- APIUtils.emptyArrayResponse
+ async (rq: express.Request, rs: express.Response) => {
+ if (typeof rq.body.Ids == 'object') {
+ const progressions = rq.body.Ids
+ .map(id => parseInt(id)).filter(id => !isNaN(id)) // filter out non-numbers
+ .map(id => UnifiedProfile.get(id).Progression.export()); // get all progressions
+ rs.json(await Promise.all(progressions));
+ } else {
+ const id = parseInt(rq.body.Ids);
+ if (isNaN(id)) {
+ rs.sendStatus(400);
+ return;
+ }
+ rs.json([await UnifiedProfile.get(id).Progression.export()]);
+ }
+ },
);
\ No newline at end of file
diff --git a/src/routes/api/quickplay.ts b/src/routes/api/quickplay.ts
index d401eca..fd7073e 100644
--- a/src/routes/api/quickplay.ts
+++ b/src/routes/api/quickplay.ts
@@ -16,17 +16,21 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
import { APIUtils } from "../../apiutils.ts";
+import { Config } from "../../config.ts";
import { AuthType } from "../../data/users.ts";
export const route = APIUtils.createRouter("/quickPlay");
+const config = Config.getConfig();
+
route.router.get('/v1/getandclear',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
(_rq, rs) => {
- rs.json({});
+ if (!config.public.initialRoom) rs.json({});
+ else rs.json({ RoomName: config.public.initialRoom });
}
);
\ No newline at end of file
diff --git a/src/routes/api/rooms.ts b/src/routes/api/rooms.ts
index 991ae40..d90f474 100644
--- a/src/routes/api/rooms.ts
+++ b/src/routes/api/rooms.ts
@@ -17,7 +17,7 @@ along with this program. If not, see . */
import { APIUtils } from "../../apiutils.ts";
import Rooms from "../../data/content/rooms.ts";
-import { RoomAccessibility } from "../../data/content/roomtypes.ts";
+import { RoomDataTypes } from "../../data/content/rooms/DataTypes.ts";
import { AuthType } from "../../data/users.ts";
import express from "express";
@@ -66,7 +66,7 @@ route.router.get('/v1/hot',
async (_rq, rs) => {
// temporary: return all public AG rooms for testing
const rooms = await Rooms.getAllBuiltinRoomGenerations();
- rs.json(rooms.map(room => room.Room).filter(room => room.Accessibility == RoomAccessibility.Public));
+ rs.json(rooms.map(room => room.Room).filter(room => room.Accessibility == RoomDataTypes.RoomAccessibility.Public));
},
);
diff --git a/src/routes/match/player.ts b/src/routes/match/player.ts
index fe5bd3d..06dc66c 100644
--- a/src/routes/match/player.ts
+++ b/src/routes/match/player.ts
@@ -118,7 +118,6 @@ route.router.put('/statusvisibility',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
express.urlencoded({ extended: true }),
- APIUtils.logBody,
APIUtils.validateRequestBody(StatusVisibilitySchema),
async (rq: express.Request, rs: express.Response) => {