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) => {