Frostbite is gone????
Some checks failed
Galvanic Corrosion Cross-Compile / build (push) Failing after 38s
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:
@@ -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>
|
||||
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
|
||||
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
49
res/roomgen.ts
Normal 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",
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,10 +17,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
await Rooms.#setRoom(beforeRoom);
|
||||
return beforeRoom;
|
||||
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<string[]>;
|
||||
async getSubroomIdsFromRoom(id: number, stringify: false): Promise<number[]>;
|
||||
async getSubroomIdsFromRoom(id: number, stringify: boolean | undefined = false): Promise<string[] | number[]> {
|
||||
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));
|
||||
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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,
|
||||
@@ -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 {
|
||||
@@ -170,3 +135,57 @@ export interface RoomDetails {
|
||||
VisitCount: number,
|
||||
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";
|
||||
359
src/data/content/rooms/RoomFactory.ts
Normal file
359
src/data/content/rooms/RoomFactory.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
227
src/data/content/rooms/SubroomFactory.ts
Normal file
227
src/data/content/rooms/SubroomFactory.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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/>. */
|
||||
|
||||
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);
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -31,7 +31,7 @@ export class ProfileReputationManager extends ProfileContentManager {
|
||||
CheerCredit: 0,
|
||||
SubscriberCount: 0,
|
||||
SubscribedCount: 0,
|
||||
SelectedCheer: 0
|
||||
SelectedCheer: null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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://www.gnu.org/licenses/>. */
|
||||
|
||||
// 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"
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -63,3 +64,4 @@ 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);
|
||||
route.router.use(AnnouncementRoute.path, AnnouncementRoute.router);
|
||||
30
src/routes/api/announcement.ts
Normal file
30
src/routes/api/announcement.ts
Normal 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
|
||||
|
||||
);
|
||||
@@ -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 <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 { 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<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()]);
|
||||
}
|
||||
},
|
||||
|
||||
);
|
||||
@@ -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/>. */
|
||||
|
||||
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<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()]);
|
||||
}
|
||||
},
|
||||
|
||||
);
|
||||
@@ -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/>. */
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
);
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
);
|
||||
|
||||
@@ -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<NoBody, NoBody, StatusVisibilityBody>, rs: express.Response) => {
|
||||
|
||||
Reference in New Issue
Block a user