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

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

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 { RootPath } from "./baseimages.ts";
import { BuiltinRoom, RoomAccessibility, RoomDetails, RoomState } from "./roomtypes.ts";
import { RoomFetch } from "./room.ts";
import { Profile } from "../profiles.ts";
import Logging from "@proxnet/undead-logging";
import { BuiltinRoom, FactoryMode, IntegratedRoomScene, RoomAccessibility, RoomDetails, RoomState, WriteMode } from "./rooms/DataTypes.ts";
import { RoomFactory } from "./rooms/RoomFactory.ts";
import { SubroomFactory } from "./rooms/SubroomFactory.ts";
import { Image } from "https://deno.land/x/imagescript@1.3.0/ImageScript.js";
const log = new Logging("Rooms");
@@ -28,51 +30,9 @@ const rooms = JSON.parse(Deno.readTextFileSync(`${RootPath}/res/rooms.json`)) as
class RoomsBase {
readonly roomMetaKeys = { // hash keys
RoomId: "id",
Name: "name",
Description: "desc",
CreatorPlayerId: "creatorId",
ImageName: "imagename",
State: "state",
Accessibility: "access",
SupportsLevelVoting: "levelvoting",
IsAGRoom: "isagroom",
IsDormRoom: "isdorm",
CloningAllowed: "cloneable",
SupportsScreens: "can-screen",
SupportsWalkVR: "can-walkvr",
SupportsTeleportVR: "can-televr",
AllowsJuniors: "juniors",
RoomWarningMask: "warningmask",
CustomRoomWarning: "warning",
DisableMicAutoMute: "disableautomute"
}
readonly subroomMetaKeys = { // hash keys
Name: "name",
RoomSceneLocationId: "location",
IsSandbox: "issandbox",
CanMatchmakeInto: "matchmakeable",
RoomSceneId: "sceneid",
DataBlobName: "datablob",
MaxPlayers: "playercap",
DataModifiedAt: "modifiedat"
}
readonly roomRootKeys = {
CheerCount: "cheers", // string
CheerPids: "cheers-players", // set
VisitCount: "visits", // string
FavoriteCount: "favorites", // string
FavoritePids: "favorites-players", // set
Subrooms: "subrooms", // set
Meta: "roommeta" // hash
}
readonly subroomRootKeys = {
Meta: "scenemeta"
}
readonly miscKeys = {
static Keys = {
BuiltinGenerated: "builtinrooms-done",
AGRooms: "agrooms"
AGRooms: "agrooms",
}
getAllBuiltinRooms() {
@@ -81,7 +41,9 @@ class RoomsBase {
async get(id: number) {
try {
return await new RoomFetch({ roomId: id }).fetch();
const factory = await new RoomFactory({ id: id }).init();
if (!factory) return null;
return factory.export();
} catch {
return null;
}
@@ -89,21 +51,23 @@ class RoomsBase {
async getByName(name: string) {
try {
return await new RoomFetch({ roomName: name }).fetch();
const factory = await new RoomFactory({ name: name }).init();
if (!factory) return null;
return factory.export();
} catch {
return null;
}
}
async getAllBuiltinRoomGenerations() {
const ids = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.AGRooms));
const ids = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.AGRooms));
const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val));
return (await Promise.all(parsedIds.map(id => this.get(id)))).filter(val => val !== null);
}
async #getAvailableRoomId() {
let id = Math.round(Math.random() * Math.pow(2, 31));
while ((await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Rooms.Root, id.toString(), this.roomRootKeys.Meta))) >= 1)
while ((await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Rooms.Root, id.toString(), RoomFactory.Keys.Meta))) >= 1)
id = await this.#getAvailableRoomId();
return id;
}
@@ -114,94 +78,76 @@ class RoomsBase {
Redis.buildKey(
Redis.KeyGroups.Rooms.Root,
roomid.toString(),
this.roomRootKeys.Subrooms,
RoomFactory.Keys.Subrooms,
id.toString(),
this.subroomRootKeys.Meta
SubroomFactory.Keys.Meta
))) >= 1)
id = await this.#getAvailableSubRoomId(roomid);
return id;
}
async #setRoom(details: RoomDetails) {
const rootKey = Redis.buildKey(Redis.KeyGroups.Rooms.Root, details.Room.RoomId.toString());
const roomMetaRootKey = Redis.buildKey(rootKey, this.roomRootKeys.Meta);
await Promise.all([
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.RoomId, details.Room.RoomId),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Name, details.Room.Name),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Description, details.Room.Description),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CreatorPlayerId, details.Room.CreatorPlayerId),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.ImageName, details.Room.ImageName),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.State, details.Room.State),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Accessibility, details.Room.Accessibility),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsLevelVoting, `${details.Room.SupportsLevelVoting}`),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.IsAGRoom, `${details.Room.IsAGRoom}`),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.IsDormRoom, `${details.Room.IsDormRoom}`),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CloningAllowed, `${details.Room.CloningAllowed}`),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsScreens, `${details.Room.SupportsScreens}`),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsWalkVR, `${details.Room.SupportsWalkVR}`),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsTeleportVR, `${details.Room.SupportsTeleportVR}`),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.AllowsJuniors, `${details.Room.AllowsJuniors}`),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.RoomWarningMask, details.Room.RoomWarningMask),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CustomRoomWarning, details.Room.CustomRoomWarning),
Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.DisableMicAutoMute, `${details.Room.DisableMicAutoMute ? details.Room.DisableMicAutoMute : 'null'}`),
]);
for (const subroom of details.Scenes) {
const newSubId = await this.#getAvailableSubRoomId(details.Room.RoomId);
const subRootMetaKey = Redis.buildKey(
Redis.KeyGroups.Rooms.Root,
details.Room.RoomId.toString(),
this.roomRootKeys.Subrooms,
newSubId.toString(),
this.subroomRootKeys.Meta
);
await Promise.all([
Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.RoomSceneLocationId, subroom.RoomSceneLocationId),
Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.Name, subroom.Name),
Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.IsSandbox, `${subroom.IsSandbox}`),
Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.DataBlobName, subroom.DataBlobName),
Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.MaxPlayers, subroom.MaxPlayers),
Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.CanMatchmakeInto, `${subroom.CanMatchmakeInto}`),
Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.DataModifiedAt, new Date().toISOString()),
]);
await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, details.Room.RoomId.toString(), this.roomRootKeys.Subrooms), newSubId);
}
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Room_Names, details.Room.Name), details.Room.RoomId);
}
async cloneRoom(roomid: number, newname: string, newowner: Profile) {
const canBeClonedRaw = await Redis.Database.hget(Redis.buildKey(
Redis.KeyGroups.Rooms.Root,
roomid.toString(),
Rooms.roomRootKeys.Meta
), Rooms.roomMetaKeys.CloningAllowed);
if (!canBeClonedRaw) return null;
let canBeCloned = null;
try {
canBeCloned = JSON.parse(canBeClonedRaw) as boolean;
} catch {
log.d(`Cloneroom ${roomid}: parse error`);
return null;
enum CloneResult {
Success,
DoesNotAllowCloning,
CannotCloneDormRoom,
NameIsTaken,
Unknown
}
if (!canBeCloned) {
log.d(`Cloneroom ${roomid}: cannot be cloned`);
return null;
interface RoomClone {
factory?: RoomFactory;
result: CloneResult;
}
const beforeRoom = await Rooms.get(roomid); // room must exist
if (!beforeRoom || !beforeRoom.Room.CloningAllowed) return null; // room must be cloneable
if (beforeRoom.Room.Name !== 'DormRoom' && await Rooms.getByName(newname)) return null; // room name cannot be taken
const newId = await this.#getAvailableRoomId();
beforeRoom.Room.CreatorPlayerId = newowner.getId();
beforeRoom.Room.RoomId = newId;
for (const subroom of beforeRoom.Scenes) subroom.RoomId = newId;
await Rooms.#setRoom(beforeRoom);
return beforeRoom;
const factory = await new RoomFactory({ id: roomid, factoryMode: FactoryMode.Fetch }).init();
if (!factory || !factory.CloningAllowed) return { result: CloneResult.DoesNotAllowCloning } as RoomClone;
if (factory.Name == 'DormRoom') return { result: CloneResult.CannotCloneDormRoom } as RoomClone;
if (factory.Name == newname) return { result: CloneResult.NameIsTaken } as RoomClone;
const newFactory = await new RoomFactory({ id: await Rooms.#getAvailableRoomId(), factoryMode: FactoryMode.Write }).init();
if (!newFactory) return { result: CloneResult.Unknown } as RoomClone;
newFactory.CreatorPlayerId = newowner.getId();
newFactory.Description = factory.Description;
newFactory.Name = factory.Name;
newFactory.ImageName = factory.Description;
newFactory.State = factory.State;
newFactory.RoomAccessibility = factory.RoomAccessibility;
newFactory.SupportsLevelVoting = factory.SupportsLevelVoting;
newFactory.IsAGRoom = factory.IsAGRoom;
newFactory.IsDormRoom = factory.IsDormRoom;
newFactory.CloningAllowed = false; // new rooms cannot be cloned
newFactory.AllowsJuniors = factory.AllowsJuniors;
newFactory.RoomWarningMask = factory.RoomWarningMask;
newFactory.CustomRoomWarning = factory.CustomRoomWarning;
newFactory.DisableMicAutoMute = factory.DisableMicAutoMute;
const oldHardware = await factory.getHardwareSupport();
const hardwarePromises = oldHardware.map(hw => newFactory.addHardwareSupport(hw));
await Promise.all(hardwarePromises);
const oldSubroomIds = await factory.getAllSubroomIds();
const promises = oldSubroomIds.map(async (id) => {
const newSubroomFactory = newFactory.getSubroom(id, FactoryMode.Write, WriteMode.Overwrite);
const oldSubroomFactory = factory.getSubroom(id, FactoryMode.Fetch);
newSubroomFactory.RoomSceneLocationId = oldSubroomFactory.RoomSceneLocationId;
newSubroomFactory.Name = oldSubroomFactory.Name;
newSubroomFactory.IsSandbox = oldSubroomFactory.IsSandbox;
newSubroomFactory.DataBlobName = oldSubroomFactory.DataBlobName;
newSubroomFactory.MaxPlayers = oldSubroomFactory.MaxPlayers;
newSubroomFactory.CanMatchmakeInto = oldSubroomFactory.CanMatchmakeInto;
await newSubroomFactory.write();
});
await Promise.all(promises);
await newFactory.write();
return {
factory: newFactory,
result: CloneResult.Success
} as RoomClone
}
async getProfileDormDefault(player: Profile) {
@@ -209,131 +155,127 @@ class RoomsBase {
Redis.KeyGroups.Rooms.Root,
Redis.KeyGroups.Rooms.PlayerDorms
), player.getId().toString());
if (unparsedId) {
log.d(`Unparsed dorm ID for profile ${player.getId()}: ${unparsedId}`);
const parsedId = parseInt(unparsedId);
if (isNaN(parsedId)) {
log.d(`Returning new dorm for profile ${player.getId()}`);
if (!isNaN(parsedId)) {
log.d(`Returning existing dorm for profile ${player.getId()}`);
return await Rooms.get(parsedId);
}
}
const newDorm = await this.generateNewDorm(player);
await this.#setRoom(newDorm);
log.d(`New dorm for ${player.getId()} existed`);
if (!newDorm) return null;
log.d(`New dorm for ${player.getId()} was not null`);
await Redis.Database.hset(Redis.buildKey(
Redis.KeyGroups.Rooms.Root,
Redis.KeyGroups.Rooms.PlayerDorms
), player.getId().toString(), newDorm.Room.RoomId);
return newDorm;
), player.getId().toString(), newDorm.RoomId);
return await newDorm.export();
}
async generateNewDorm(player: Profile) {
const id = await this.#getAvailableRoomId();
const basedorm: RoomDetails = {
Room: {
RoomId: id,
Name: `DormRoom`,
Description: "Your private room.",
CreatorPlayerId: player.getId(),
ImageName: "DefaultProfileImage.png",
State: RoomState.Active,
Accessibility: RoomAccessibility.Private,
SupportsLevelVoting: false,
IsAGRoom: false,
IsDormRoom: true,
CloningAllowed: false,
SupportsScreens: true,
SupportsTeleportVR: true,
SupportsWalkVR: true,
AllowsJuniors: true,
RoomWarningMask: 0,
CustomRoomWarning: "",
DisableMicAutoMute: false
},
Scenes: [
{
RoomId: id,
RoomSceneId: 1,
Name: "Home",
RoomSceneLocationId: "76d98498-60a1-430c-ab76-b54a29b7a163",
IsSandbox: true,
CanMatchmakeInto: true,
MaxPlayers: 4,
DataBlobName: "",
DataModifiedAt: new Date().toISOString()
}
],
CoOwners: [],
InvitedCoOwners: [],
Hosts: [],
InvitedHosts: [],
CheerCount: 0,
VisitCount: 0,
FavoriteCount: 0,
Tags: []
}
return basedorm;
const factory = await new RoomFactory({ id: id, factoryMode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree }).init();
if (!factory) return null;
factory.Name = "DormRoom";
factory.Description = "Your private room.";
factory.CreatorPlayerId = player.getId();
factory.ImageName = "DefaultProfileImage.png";
factory.State = RoomState.Active;
factory.RoomAccessibility = RoomAccessibility.Private;
factory.SupportsLevelVoting = false;
factory.IsAGRoom = false;
factory.IsDormRoom = true;
factory.CloningAllowed = false;
factory.AllowsJuniors = true;
factory.RoomWarningMask = 0;
factory.CustomRoomWarning = "";
factory.addHardwareSupport('*');
const subroomFactory = factory.getSubroom(await this.#getAvailableSubRoomId(id), FactoryMode.Write, WriteMode.WriteIfFree);
if (!subroomFactory) return null;
subroomFactory.RoomSceneLocationId = IntegratedRoomScene.DormRoom;
subroomFactory.Name = "Home";
subroomFactory.IsSandbox = true;
subroomFactory.DataBlobName = "";
subroomFactory.MaxPlayers = 4;
subroomFactory.CanMatchmakeInto = true;
factory.write();
subroomFactory.write();
return factory;
}
async generateBuiltinRooms() {
if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.BuiltinGenerated))) !== null) return true;
for (const builtinRoom of rooms) {
if (builtinRoom.Name == 'DormRoom') continue;
if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.BuiltinGenerated))) !== null) return true;
await Promise.all(rooms.map(async builtinRoom => {
if (builtinRoom.Name == 'DormRoom') return;
const newId = await this.#getAvailableRoomId();
await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.AGRooms), newId);
const roomDets: RoomDetails = {
Room: {
Name: builtinRoom.Name,
RoomId: newId,
Description: builtinRoom.Description,
CreatorPlayerId: 1,
ImageName: `${builtinRoom.Name}.png`,
State: RoomState.Active,
Accessibility: builtinRoom.Accessibility,
SupportsLevelVoting: builtinRoom.SupportsLevelVoting,
IsAGRoom: true,
IsDormRoom: builtinRoom.Name == 'DormRoom',
CloningAllowed: builtinRoom.Name == 'DormRoom' ? true : builtinRoom.CloningAllowed,
SupportsScreens: builtinRoom.SupportsScreens,
SupportsWalkVR: builtinRoom.SupportsWalkVR,
SupportsTeleportVR: builtinRoom.SupportsTeleportVR,
AllowsJuniors: true,
RoomWarningMask: 0,
CustomRoomWarning: ""
},
Scenes: [],
CoOwners: [],
InvitedCoOwners: [],
Hosts: [],
InvitedHosts: [],
CheerCount: 0,
FavoriteCount: 0,
VisitCount: 0,
Tags: []
}
for (const subroom of builtinRoom.Scenes) {
const newSubroomId = await this.#getAvailableSubRoomId(newId);
roomDets.Scenes.push({
RoomSceneId: newSubroomId,
RoomId: newId,
RoomSceneLocationId: subroom.RoomSceneLocationId,
Name: subroom.Name,
IsSandbox: subroom.IsSandbox,
DataBlobName: "",
MaxPlayers: subroom.MaxPlayers,
CanMatchmakeInto: subroom.CanMatchmakeInto ? subroom.CanMatchmakeInto : true,
DataModifiedAt: new Date().toISOString()
});
}
await this.#setRoom(roomDets);
}
Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.BuiltinGenerated), "1");
return false;
await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.AGRooms), newId);
const factory = await new RoomFactory({ id: newId, factoryMode: FactoryMode.Write, writeMode: WriteMode.Overwrite }).init();
if (!factory) return;
factory.Name = builtinRoom.Name;
factory.Description = builtinRoom.Description;
factory.CreatorPlayerId = 1;
const baseImageChanges = [
{ room: "DodgeballVR", image: "Dodgeball" },
{ room: "PaintballVR", image: "Paintball" },
{ room: "StuntRunnerBaseRoom", image: "StuntRunner" },
{ room: "BowlingAlley", image: "Bowling" },
]
if (baseImageChanges.find(change => change.room == builtinRoom.Name)) {
const image = baseImageChanges.find(change => change.room == builtinRoom.Name)!;
factory.ImageName = `${image.image}.png`;
}
else factory.ImageName = `${builtinRoom.Name}.png`;
factory.State = RoomState.Active;
factory.RoomAccessibility = builtinRoom.Accessibility;
factory.SupportsLevelVoting = builtinRoom.SupportsLevelVoting;
factory.IsAGRoom = true;
factory.CloningAllowed = builtinRoom.CloningAllowed;
factory.AllowsJuniors = true;
factory.RoomWarningMask = 0;
factory.CustomRoomWarning = "";
if (builtinRoom.SupportsScreens) factory.addHardwareSupport('screens');
if (builtinRoom.SupportsTeleportVR) factory.addHardwareSupport('teleport_vr');
if (builtinRoom.SupportsWalkVR) factory.addHardwareSupport('walk_vr');
await Promise.all(builtinRoom.Scenes.map(async subroom => {
const newSubroomId = await this.#getAvailableSubRoomId(newId);
const subroomFactory = await factory.getSubroom(newSubroomId, FactoryMode.Write, WriteMode.Overwrite).init();
if (!subroomFactory) return;
subroomFactory.RoomSceneLocationId = subroom.RoomSceneLocationId;
subroomFactory.Name = subroom.Name;
subroomFactory.IsSandbox = subroom.IsSandbox;
subroomFactory.DataBlobName = "";
subroomFactory.MaxPlayers = subroom.MaxPlayers;
subroomFactory.CanMatchmakeInto = subroom.CanMatchmakeInto;
await subroomFactory.write();
}));
await factory.write();
}));
Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.BuiltinGenerated), "1");
return false;
}
async getIdFromName(name: string) {
@@ -344,23 +286,13 @@ class RoomsBase {
return parsedId;
}
async getNameFromId(id: number) {
const name = await Redis.Database.hget(Redis.buildKey(
Redis.KeyGroups.Rooms.Root,
id.toString(),
this.roomRootKeys.Meta
), this.roomMetaKeys.Name);
if (!name) return null;
return name;
}
async getSubroomIdsFromRoom(id: number): Promise<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));

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
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,
@@ -72,9 +37,9 @@ export enum RoomState {
}
export enum RoomAccessibility {
Private,
Public,
Unlisted
Private,
Public,
Unlisted
}
export interface BuiltinScene {
@@ -131,7 +96,7 @@ export interface Room {
AllowsJuniors: boolean,
RoomWarningMask: number, // generated by dedicated mask generation function
CustomRoomWarning: string,
DisableMicAutoMute?: boolean
DisableMicAutoMute?: boolean | null
}
export enum RoomWarningMask {
@@ -169,4 +134,58 @@ export interface RoomDetails {
FavoriteCount: number,
VisitCount: number,
Tags: TagDTO[]
}
}
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/>. */
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);

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

View File

@@ -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() {

View File

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

View File

@@ -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() {

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
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"
],
};

View File

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

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 PlayerEventsRoute } from "./api/playerevents.ts";
import { route as StorefrontsRoute } from "./api/storefronts.ts";
import { route as AnnouncementRoute } from "./api/announcement.ts";
export const route = APIUtils.createRouter("/api");
@@ -62,4 +63,5 @@ route.router.use(ChecklistRoute.path, ChecklistRoute.router);
route.router.use(ImagesRoute.path, ImagesRoute.router);
route.router.use(CommunityBoardRoute.path, CommunityBoardRoute.router);
route.router.use(PlayerEventsRoute.path, PlayerEventsRoute.router);
route.router.use(StorefrontsRoute.path, StorefrontsRoute.router);
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
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()]);
}
},
);

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/>. */
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()]);
}
},
);

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/>. */
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 });
}
);

View File

@@ -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));
},
);

View File

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