Frostbite is gone????
Some checks failed
Galvanic Corrosion Cross-Compile / build (push) Failing after 38s

* Rewrite rooms backend, "RoomFactory" and "SubroomFactory"
    - Used for modifying and fetching rooms
* Progression and reputation bulk endpoints
* Announcement endpoint temp
* OOBE is now the only initial pref key
    - Will be removed in the future when cohortnux is implemented
* Misc minor fixes and clarifications
* Simplified namegen dictionary
    - The previous one was generated with ChatGPT, hence the duplicated strings. I googled "random username generator" and borrowed a random result's generation dictionary.
* QuickPlay support with "initialRoom" in config (untested)
This commit is contained in:
2025-04-15 21:15:15 -04:00
parent 1672f2af91
commit 5c69269b70
22 changed files with 1021 additions and 680 deletions

View File

@@ -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,<br> `photonRegionId`: The region to connect to when using Photon Cloud. When using the self-hosted PhotonSocketServer,<br>
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. 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.<br>
Ideally, this room should not be private and should be matchmakeable.
## Logging ## 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. These three values expose booleans you can change to enable/disable logging various messages sent by the server used for debugging or troubleshooting purposes.

49
res/roomgen.ts Normal file
View File

@@ -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",

View File

@@ -50,6 +50,7 @@ type PublicConfiguration = {
maxLevels: number; maxLevels: number;
patches: string[]; patches: string[];
photonRegionId: PhotonRegionCodeString | PhotonRegionCodeNumber; photonRegionId: PhotonRegionCodeString | PhotonRegionCodeNumber;
initialRoom: string | null;
}; };
type LoggingConfiguration = { type LoggingConfiguration = {
@@ -112,6 +113,7 @@ export const defaultConfig: GalvanicConfiguration = {
maxLevels: 30, maxLevels: 30,
patches: [], patches: [],
photonRegionId: PhotonRegionCodeNumber.us, photonRegionId: PhotonRegionCodeNumber.us,
initialRoom: null
}, },
logging: { logging: {
notfound: false, notfound: false,

View File

@@ -1,132 +0,0 @@
/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
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 <https://www.gnu.org/licenses/>. */
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;
}
}

View File

@@ -17,10 +17,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { Redis } from "../../db.ts"; import { Redis } from "../../db.ts";
import { RootPath } from "./baseimages.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 { Profile } from "../profiles.ts";
import Logging from "@proxnet/undead-logging"; 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"); const log = new Logging("Rooms");
@@ -28,51 +30,9 @@ const rooms = JSON.parse(Deno.readTextFileSync(`${RootPath}/res/rooms.json`)) as
class RoomsBase { class RoomsBase {
readonly roomMetaKeys = { // hash keys static 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 = {
BuiltinGenerated: "builtinrooms-done", BuiltinGenerated: "builtinrooms-done",
AGRooms: "agrooms" AGRooms: "agrooms",
} }
getAllBuiltinRooms() { getAllBuiltinRooms() {
@@ -81,7 +41,9 @@ class RoomsBase {
async get(id: number) { async get(id: number) {
try { try {
return await new RoomFetch({ roomId: id }).fetch(); const factory = await new RoomFactory({ id: id }).init();
if (!factory) return null;
return factory.export();
} catch { } catch {
return null; return null;
} }
@@ -89,21 +51,23 @@ class RoomsBase {
async getByName(name: string) { async getByName(name: string) {
try { try {
return await new RoomFetch({ roomName: name }).fetch(); const factory = await new RoomFactory({ name: name }).init();
if (!factory) return null;
return factory.export();
} catch { } catch {
return null; return null;
} }
} }
async getAllBuiltinRoomGenerations() { 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)); 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); return (await Promise.all(parsedIds.map(id => this.get(id)))).filter(val => val !== null);
} }
async #getAvailableRoomId() { async #getAvailableRoomId() {
let id = Math.round(Math.random() * Math.pow(2, 31)); 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(); id = await this.#getAvailableRoomId();
return id; return id;
} }
@@ -114,94 +78,76 @@ class RoomsBase {
Redis.buildKey( Redis.buildKey(
Redis.KeyGroups.Rooms.Root, Redis.KeyGroups.Rooms.Root,
roomid.toString(), roomid.toString(),
this.roomRootKeys.Subrooms, RoomFactory.Keys.Subrooms,
id.toString(), id.toString(),
this.subroomRootKeys.Meta SubroomFactory.Keys.Meta
))) >= 1) ))) >= 1)
id = await this.#getAvailableSubRoomId(roomid); id = await this.#getAvailableSubRoomId(roomid);
return id; 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) { async cloneRoom(roomid: number, newname: string, newowner: Profile) {
const canBeClonedRaw = await Redis.Database.hget(Redis.buildKey( enum CloneResult {
Redis.KeyGroups.Rooms.Root, Success,
roomid.toString(), DoesNotAllowCloning,
Rooms.roomRootKeys.Meta CannotCloneDormRoom,
), Rooms.roomMetaKeys.CloningAllowed); NameIsTaken,
if (!canBeClonedRaw) return null; Unknown
let canBeCloned = null;
try {
canBeCloned = JSON.parse(canBeClonedRaw) as boolean;
} catch {
log.d(`Cloneroom ${roomid}: parse error`);
return null;
} }
if (!canBeCloned) { interface RoomClone {
log.d(`Cloneroom ${roomid}: cannot be cloned`); factory?: RoomFactory;
return null; 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(); const factory = await new RoomFactory({ id: roomid, factoryMode: FactoryMode.Fetch }).init();
beforeRoom.Room.CreatorPlayerId = newowner.getId(); if (!factory || !factory.CloningAllowed) return { result: CloneResult.DoesNotAllowCloning } as RoomClone;
beforeRoom.Room.RoomId = newId; if (factory.Name == 'DormRoom') return { result: CloneResult.CannotCloneDormRoom } as RoomClone;
for (const subroom of beforeRoom.Scenes) subroom.RoomId = newId; if (factory.Name == newname) return { result: CloneResult.NameIsTaken } as RoomClone;
await Rooms.#setRoom(beforeRoom); const newFactory = await new RoomFactory({ id: await Rooms.#getAvailableRoomId(), factoryMode: FactoryMode.Write }).init();
return beforeRoom; 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) { async getProfileDormDefault(player: Profile) {
@@ -209,131 +155,127 @@ class RoomsBase {
Redis.KeyGroups.Rooms.Root, Redis.KeyGroups.Rooms.Root,
Redis.KeyGroups.Rooms.PlayerDorms Redis.KeyGroups.Rooms.PlayerDorms
), player.getId().toString()); ), player.getId().toString());
if (unparsedId) { if (unparsedId) {
log.d(`Unparsed dorm ID for profile ${player.getId()}: ${unparsedId}`); log.d(`Unparsed dorm ID for profile ${player.getId()}: ${unparsedId}`);
const parsedId = parseInt(unparsedId); const parsedId = parseInt(unparsedId);
if (isNaN(parsedId)) { if (!isNaN(parsedId)) {
log.d(`Returning new dorm for profile ${player.getId()}`); log.d(`Returning existing dorm for profile ${player.getId()}`);
return await Rooms.get(parsedId); return await Rooms.get(parsedId);
} }
} }
const newDorm = await this.generateNewDorm(player); const newDorm = await this.generateNewDorm(player);
await this.#setRoom(newDorm);
log.d(`New dorm for ${player.getId()} existed`); log.d(`New dorm for ${player.getId()} existed`);
if (!newDorm) return null; if (!newDorm) return null;
log.d(`New dorm for ${player.getId()} was not null`); log.d(`New dorm for ${player.getId()} was not null`);
await Redis.Database.hset(Redis.buildKey( await Redis.Database.hset(Redis.buildKey(
Redis.KeyGroups.Rooms.Root, Redis.KeyGroups.Rooms.Root,
Redis.KeyGroups.Rooms.PlayerDorms Redis.KeyGroups.Rooms.PlayerDorms
), player.getId().toString(), newDorm.Room.RoomId); ), player.getId().toString(), newDorm.RoomId);
return newDorm;
return await newDorm.export();
} }
async generateNewDorm(player: Profile) { async generateNewDorm(player: Profile) {
const id = await this.#getAvailableRoomId(); const id = await this.#getAvailableRoomId();
const basedorm: RoomDetails = {
Room: { const factory = await new RoomFactory({ id: id, factoryMode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree }).init();
RoomId: id, if (!factory) return null;
Name: `DormRoom`,
Description: "Your private room.", factory.Name = "DormRoom";
CreatorPlayerId: player.getId(), factory.Description = "Your private room.";
ImageName: "DefaultProfileImage.png", factory.CreatorPlayerId = player.getId();
State: RoomState.Active, factory.ImageName = "DefaultProfileImage.png";
Accessibility: RoomAccessibility.Private, factory.State = RoomState.Active;
SupportsLevelVoting: false, factory.RoomAccessibility = RoomAccessibility.Private;
IsAGRoom: false, factory.SupportsLevelVoting = false;
IsDormRoom: true, factory.IsAGRoom = false;
CloningAllowed: false, factory.IsDormRoom = true;
SupportsScreens: true, factory.CloningAllowed = false;
SupportsTeleportVR: true, factory.AllowsJuniors = true;
SupportsWalkVR: true, factory.RoomWarningMask = 0;
AllowsJuniors: true, factory.CustomRoomWarning = "";
RoomWarningMask: 0,
CustomRoomWarning: "", factory.addHardwareSupport('*');
DisableMicAutoMute: false
}, const subroomFactory = factory.getSubroom(await this.#getAvailableSubRoomId(id), FactoryMode.Write, WriteMode.WriteIfFree);
Scenes: [ if (!subroomFactory) return null;
{
RoomId: id, subroomFactory.RoomSceneLocationId = IntegratedRoomScene.DormRoom;
RoomSceneId: 1, subroomFactory.Name = "Home";
Name: "Home", subroomFactory.IsSandbox = true;
RoomSceneLocationId: "76d98498-60a1-430c-ab76-b54a29b7a163", subroomFactory.DataBlobName = "";
IsSandbox: true, subroomFactory.MaxPlayers = 4;
CanMatchmakeInto: true, subroomFactory.CanMatchmakeInto = true;
MaxPlayers: 4,
DataBlobName: "", factory.write();
DataModifiedAt: new Date().toISOString() subroomFactory.write();
}
], return factory;
CoOwners: [],
InvitedCoOwners: [],
Hosts: [],
InvitedHosts: [],
CheerCount: 0,
VisitCount: 0,
FavoriteCount: 0,
Tags: []
}
return basedorm;
} }
async generateBuiltinRooms() { async generateBuiltinRooms() {
if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.BuiltinGenerated))) !== null) return true; if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.BuiltinGenerated))) !== null) return true;
for (const builtinRoom of rooms) { await Promise.all(rooms.map(async builtinRoom => {
if (builtinRoom.Name == 'DormRoom') continue; if (builtinRoom.Name == 'DormRoom') return;
const newId = await this.#getAvailableRoomId(); const newId = await this.#getAvailableRoomId();
await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.AGRooms), newId); await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.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;
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) { async getIdFromName(name: string) {
@@ -344,23 +286,13 @@ class RoomsBase {
return parsedId; 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<string[]>; async getSubroomIdsFromRoom(id: number): Promise<string[]>;
async getSubroomIdsFromRoom(id: number, stringify: false): Promise<number[]>; async getSubroomIdsFromRoom(id: number, stringify: false): Promise<number[]>;
async getSubroomIdsFromRoom(id: number, stringify: boolean | undefined = false): Promise<string[] | number[]> { async getSubroomIdsFromRoom(id: number, stringify: boolean | undefined = false): Promise<string[] | number[]> {
const ids = await Redis.Database.smembers(Redis.buildKey( const ids = await Redis.Database.smembers(Redis.buildKey(
Redis.KeyGroups.Rooms.Root, Redis.KeyGroups.Rooms.Root,
id.toString(), id.toString(),
this.roomRootKeys.Subrooms RoomFactory.Keys.Subrooms
)); ));
const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val)); const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val));

View File

@@ -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 You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
export enum IntegratedRoomScene { export enum WriteMode {
Calibration = "f5fbd9c9-e853-4036-9d48-5f68e861af04", Overwrite = "overwrite",
DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163", WriteIfFree = "if_free"
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 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 { export enum RoomState {
Active, Active,
PendingJunior = 11, PendingJunior = 11,
@@ -131,7 +96,7 @@ export interface Room {
AllowsJuniors: boolean, AllowsJuniors: boolean,
RoomWarningMask: number, // generated by dedicated mask generation function RoomWarningMask: number, // generated by dedicated mask generation function
CustomRoomWarning: string, CustomRoomWarning: string,
DisableMicAutoMute?: boolean DisableMicAutoMute?: boolean | null
} }
export enum RoomWarningMask { export enum RoomWarningMask {
@@ -170,3 +135,57 @@ export interface RoomDetails {
VisitCount: number, VisitCount: number,
Tags: TagDTO[] Tags: TagDTO[]
} }
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";

View File

@@ -0,0 +1,359 @@
/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
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 <https://www.gnu.org/licenses/>. */
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<string, string> | 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<HardwareSupport[]> {
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);
}
}

View File

@@ -0,0 +1,227 @@
/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
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 <https://www.gnu.org/licenses/>. */
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<string, string> | 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);
}
}

View File

@@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import Rooms from "../content/rooms.ts"; 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 { Profile } from "../profiles.ts";
import Instances from "./instances.ts"; import Instances from "./instances.ts";
import { MatchmakingErrorCode, RoomInstance } from "./types.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 // 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); const targetRoom = options.roomName !== 'DormRoom' ? await Rooms.getByName(options.roomName) : await Rooms.getProfileDormDefault(options.profile);
if (!targetRoom) return { errorCode: MatchmakingErrorCode.NoSuchRoom }; 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 }; 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; const roomId = targetRoom.Room.RoomId;
Instances.clearAllRoomEmptyInstances(roomId); Instances.clearAllRoomEmptyInstances(roomId);

View File

@@ -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 You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { RoomDetails } from "../content/roomtypes.ts"; import { RoomDataTypes } from "../content/rooms/DataTypes.ts";
import { Profile } from "../profiles.ts"; import { Profile } from "../profiles.ts";
export enum PhotonRegionCodeString { export enum PhotonRegionCodeString {
@@ -70,7 +70,7 @@ export interface RoomInstance {
export interface InstanceOptions { export interface InstanceOptions {
Room: RoomDetails, Room: RoomDataTypes.RoomDetails,
SceneIndex: number, SceneIndex: number,
EventId?: number, EventId?: number,
Name?: string, Name?: string,

View File

@@ -24,8 +24,24 @@ const log = new Logging("ProfileProgression");
const config = GameConfigs.getConfig(); const config = GameConfigs.getConfig();
interface PlayerProgressionExport {
PlayerId: number,
Level: number,
XP: number
}
export class ProfileProgressionManager extends ProfileContentManager { 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 * Set the profile's exact # of XP
* @returns The new # of XP * @returns The new # of XP
@@ -54,7 +70,7 @@ export class ProfileProgressionManager extends ProfileContentManager {
const xp = await this.getXp(); const xp = await this.getXp();
if (xp == null) return 1; // fallback since progression data is required 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) { for (const item of config?.LevelProgressionMaps) {
if (xp >= item.RequiredXp) { if (xp >= item.RequiredXp) {
@@ -64,6 +80,8 @@ export class ProfileProgressionManager extends ProfileContentManager {
} }
} }
return 1; // fallback
} }
async getXp() { async getXp() {

View File

@@ -31,7 +31,7 @@ export class ProfileReputationManager extends ProfileContentManager {
CheerCredit: 0, CheerCredit: 0,
SubscriberCount: 0, SubscriberCount: 0,
SubscribedCount: 0, SubscribedCount: 0,
SelectedCheer: 0 SelectedCheer: null
}; };
} }

View File

@@ -28,10 +28,6 @@ export class ProfileSettingsManager extends ProfileContentManager {
override async onProfileInit() { override async onProfileInit() {
await this.setSetting(SettingKey.RecroomOOBE, SettingDefault.RecroomOOBE); 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() { async getSettings() {

View File

@@ -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 You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
// https://randomusernameapi.github.io/
const Dictionary = { const Dictionary = {
Adjectives: [ Adjectives: [
"Amazing", "Alpha", "Zen", "Ruby", "Pixel", "Captain",
"Affable", "Luna", "Quantum", "Emerald", "Serene", "Sushi",
"Agreeable", "Mountain", "Phoenix", "Electric", "Songbird", "Tech",
"Ambitious", "Silver", "Midnight", "Tango", "Cosmic", "Jazz",
"Amicable", "Velvet", "Neon", "Ghostly", "Ballet", "Delta",
"Animated", "Echo", "Solar", "Pirate", "Harmonic",
"Astute", "Cyber", "Melody", "Quasar", "Crimson", "Enigma",
"Authentic", "Stardust", "Techno", "Lunar", "Rogue", "Dream"
"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",
], ],
Nouns: [ Nouns: [
"Nomad", "Wolf", "Master", "Red", "Pirate", "Adventure",
"Solstice", "Lovegood", "Coder", "Enigma", "Seeker", "Samurai",
"Elysium", "Mover", "Fire", "Echo", "Soul", "Titan",
"Horizon", "Shadow", "Mystic", "Tornado", "Crafter", "Journey",
"Catalyst", "Vortex", "Nebula", "Gazer", "Blossom", "Dynamo",
"Utopia", "Eagle", "Symphony", "Willow", "Pioneer", "Hawk",
"Eclipse", "Scribe", "Mistress", "Quest", "Comet", "Explorer",
"Nebula", "Strider", "Trance", "Lullaby", "Dancer"
"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",
], ],
}; };

View File

@@ -130,7 +130,7 @@ try {
const authHeader = req.headers.get('authorization'); const authHeader = req.headers.get('authorization');
if (!authHeader) return { valid: false } as AuthResult; 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? const token = authHeader.split(", ")[1]; // Why is the header formatted like this?
if (!token) return { valid: false } as AuthResult; if (!token) return { valid: false } as AuthResult;
const splitToken = token.split(' ')[1]; const splitToken = token.split(' ')[1];
@@ -220,6 +220,7 @@ try {
if (!(await GameConfigs.getGameConfig('splitTestSoftOverrides'))) GameConfigs.setGameConfig('splitTestSoftOverrides', ''); if (!(await GameConfigs.getGameConfig('splitTestSoftOverrides'))) GameConfigs.setGameConfig('splitTestSoftOverrides', '');
if (!(await GameConfigs.getGameConfig('splitTestHardOverrides'))) GameConfigs.setGameConfig('splitTestHardOverrides', ''); if (!(await GameConfigs.getGameConfig('splitTestHardOverrides'))) GameConfigs.setGameConfig('splitTestHardOverrides', '');
log.i('Startup done.');
}); });
http.on('error', err => { http.on('error', err => {

View File

@@ -38,6 +38,7 @@ import { route as ImagesRoute } from "./api/images.ts";
import { route as CommunityBoardRoute } from "./api/communityboard.ts"; import { route as CommunityBoardRoute } from "./api/communityboard.ts";
import { route as PlayerEventsRoute } from "./api/playerevents.ts"; import { route as PlayerEventsRoute } from "./api/playerevents.ts";
import { route as StorefrontsRoute } from "./api/storefronts.ts"; import { route as StorefrontsRoute } from "./api/storefronts.ts";
import { route as AnnouncementRoute } from "./api/announcement.ts";
export const route = APIUtils.createRouter("/api"); export const route = APIUtils.createRouter("/api");
@@ -63,3 +64,4 @@ route.router.use(ImagesRoute.path, ImagesRoute.router);
route.router.use(CommunityBoardRoute.path, CommunityBoardRoute.router); route.router.use(CommunityBoardRoute.path, CommunityBoardRoute.router);
route.router.use(PlayerEventsRoute.path, PlayerEventsRoute.router); route.router.use(PlayerEventsRoute.path, PlayerEventsRoute.router);
route.router.use(StorefrontsRoute.path, StorefrontsRoute.router); route.router.use(StorefrontsRoute.path, StorefrontsRoute.router);
route.router.use(AnnouncementRoute.path, AnnouncementRoute.router);

View File

@@ -0,0 +1,30 @@
/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
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 <https://www.gnu.org/licenses/>. */
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
);

View File

@@ -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 You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { APIUtils } from "../../apiutils.ts"; import { z } from "zod";
import { APIUtils, NoBody } from "../../apiutils.ts";
import UnifiedProfile from "../../data/profiles.ts"; import UnifiedProfile from "../../data/profiles.ts";
import { AuthType } from "../../data/users.ts"; import { AuthType } from "../../data/users.ts";
import express from "express"; 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.Authentication,
APIUtils.AuthenticationType(AuthType.Game), APIUtils.AuthenticationType(AuthType.Game),
express.urlencoded({ extended: true }), express.urlencoded({ extended: true }),
APIUtils.logBody, APIUtils.validateRequestBody(reputationBulkSchema),
APIUtils.emptyArrayResponse async (rq: express.Request<NoBody, {}, ReputationBulkBody>, 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()]);
}
},
); );

View File

@@ -16,10 +16,11 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import Logging from "@proxnet/undead-logging"; import Logging from "@proxnet/undead-logging";
import { APIUtils } from "../../apiutils.ts"; import { APIUtils, NoBody } from "../../apiutils.ts";
import express from "express"; import express from "express";
import UnifiedProfile from "../../data/profiles.ts"; import UnifiedProfile from "../../data/profiles.ts";
import { AuthType } from "../../data/users.ts"; import { AuthType } from "../../data/users.ts";
import { z } from "zod";
const log = new Logging("ProgressionRoute"); 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', route.router.post('/v1/progression/bulk',
APIUtils.Authentication, APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game), APIUtils.AuthenticationType(AuthType.Game),
express.urlencoded({ extended: true }), express.urlencoded({ extended: true }),
APIUtils.logBody, APIUtils.validateRequestBody(progressionBulkSchema),
APIUtils.emptyArrayResponse async (rq: express.Request<NoBody, {}, ProgressionBulkBody>, 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()]);
}
},
); );

View File

@@ -16,17 +16,21 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { APIUtils } from "../../apiutils.ts"; import { APIUtils } from "../../apiutils.ts";
import { Config } from "../../config.ts";
import { AuthType } from "../../data/users.ts"; import { AuthType } from "../../data/users.ts";
export const route = APIUtils.createRouter("/quickPlay"); export const route = APIUtils.createRouter("/quickPlay");
const config = Config.getConfig();
route.router.get('/v1/getandclear', route.router.get('/v1/getandclear',
APIUtils.Authentication, APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game), APIUtils.AuthenticationType(AuthType.Game),
(_rq, rs) => { (_rq, rs) => {
rs.json({}); if (!config.public.initialRoom) rs.json({});
else rs.json({ RoomName: config.public.initialRoom });
} }
); );

View File

@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { APIUtils } from "../../apiutils.ts"; import { APIUtils } from "../../apiutils.ts";
import Rooms from "../../data/content/rooms.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 { AuthType } from "../../data/users.ts";
import express from "express"; import express from "express";
@@ -66,7 +66,7 @@ route.router.get('/v1/hot',
async (_rq, rs) => { async (_rq, rs) => {
// temporary: return all public AG rooms for testing // temporary: return all public AG rooms for testing
const rooms = await Rooms.getAllBuiltinRoomGenerations(); 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));
}, },
); );

View File

@@ -118,7 +118,6 @@ route.router.put('/statusvisibility',
APIUtils.Authentication, APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game), APIUtils.AuthenticationType(AuthType.Game),
express.urlencoded({ extended: true }), express.urlencoded({ extended: true }),
APIUtils.logBody,
APIUtils.validateRequestBody(StatusVisibilitySchema), APIUtils.validateRequestBody(StatusVisibilitySchema),
async (rq: express.Request<NoBody, NoBody, StatusVisibilityBody>, rs: express.Response) => { async (rq: express.Request<NoBody, NoBody, StatusVisibilityBody>, rs: express.Response) => {