This repository has been archived on 2026-03-19. You can view files and clone it, but cannot push or open issues or pull requests.
Files
galvanic-corrosion/src/data/content/rooms.ts
zombieb 3b6d905180 Still figuring out initial matchmaking hang (FROSTBITE). Lots of other changes.
- Added missing room images
- Removed internal rooms and disallow cloning some rooms
- License fixes
- Switched target to 20200306
- Socket header fixes
- Sockets are closed upon shutdown
    * Sockets persist after being destroyed, fix
- Config changes for 20200306
- Settings initialized
- Name generation words cannot be longer than 9 characters
- Dorm generation changes and fixes
- Added some settings keys
- Matchmaking additions
    * Instances are not yet updated to be or not to be in-progress
- Instance fixes and logging
- Image operation fixes
- DisplayName route start
- Challenge route start
- Default Amplitude key (i can see althe Amplitude requests are ignored
- Rate limiting ease
- GameConfigs properly queried and sent
- Many 'bulk' endpoints were added in or around 20200306
- Objective routes started
- DormRoom redirection in v2/name
- Client doesn't care if it gets 200 when setting prefs
- Balance/storefronts started
- Matchmaking goto/room timer and fixes
- Selfhosted Photon addition on the client sends matchmaking /photonregionpings, ignore since Photon is only one server in one region
2025-04-13 01:06:23 -04:00

382 lines
16 KiB
TypeScript

/* 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 { 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";
const log = new Logging("Rooms");
const rooms = JSON.parse(Deno.readTextFileSync(`${RootPath}/res/rooms.json`)) as BuiltinRoom[];
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 = {
BuiltinGenerated: "builtinrooms-done",
AGRooms: "agrooms"
}
getAllBuiltinRooms() {
return rooms;
}
async get(id: number) {
try {
return await new RoomFetch({ roomId: id }).fetch();
} catch {
return null;
}
}
async getByName(name: string) {
try {
return await new RoomFetch({ roomName: name }).fetch();
} catch {
return null;
}
}
async getAllBuiltinRoomGenerations() {
const ids = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Rooms.Root, this.miscKeys.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)
id = await this.#getAvailableRoomId();
return id;
}
async #getAvailableSubRoomId(roomid: number) {
let id = Math.round(Math.random() * Math.pow(2, 31));
while ((await Redis.Database.exists(
Redis.buildKey(
Redis.KeyGroups.Rooms.Root,
roomid.toString(),
this.roomRootKeys.Subrooms,
id.toString(),
this.subroomRootKeys.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;
}
if (!canBeCloned) {
log.d(`Cloneroom ${roomid}: cannot be cloned`);
return null;
}
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;
}
async getProfileDormDefault(player: Profile) {
const unparsedId = await Redis.Database.hget(Redis.buildKey(
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()}`);
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;
}
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;
}
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;
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;
}
async getIdFromName(name: string) {
const unparsedId = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Room_Names, name));
if (!unparsedId) return null;
const parsedId = parseInt(unparsedId);
if (isNaN(parsedId)) return null;
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
));
const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val));
if (!stringify) return parsedIds;
else return parsedIds.map(val => val.toString());
}
getSubroomNameFromId(room: RoomDetails, subroomId: number) {
const subroom = room.Scenes.find(scene => scene.RoomSceneId == subroomId);
if (subroom) return subroom.Name;
else return null;
}
}
const Rooms = new RoomsBase();
export { rooms as BuiltinRooms };
export default Rooms;