- 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
382 lines
16 KiB
TypeScript
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; |