/* Galvanic Corrosion - Rec Room custom server for communities. 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 . */ import { z } from "zod"; import { APIUtils, NoBody } from "../../apiutils.ts"; import Rooms from "../../data/content/rooms.ts"; import { FactoryMode, RoomDataTypes, WriteMode } from "../../data/content/rooms/DataTypes.ts"; import { AuthType } from "../../data/users.ts"; import express from "express"; import { RoomFactory } from "../../data/content/rooms/RoomFactory.ts"; import { SubroomFactory } from "../../data/content/rooms/SubroomFactory.ts"; import Logging from "@proxnet/undead-logging"; const log = new Logging("RoomsRoute"); export const route = APIUtils.createRouter("/rooms"); interface Params { roomId?: string } route.router.get('/v4/details/:roomId', APIUtils.Authentication, APIUtils.AuthenticationType(AuthType.Game), async (rq: express.Request, rs: express.Response) => { if (!rq.params.roomId) { rs.sendStatus(400); return; } const parsedId = parseInt(rq.params.roomId); if (isNaN(parsedId)) { rs.sendStatus(400); return; } const room = await Rooms.get(parsedId); if (room == null) rs.sendStatus(404); else rs.json(room); }, ); route.router.get('/v2/myrooms', APIUtils.Authentication, APIUtils.AuthenticationType(AuthType.Game), async (_rq, rs) => { const ids = await rs.locals.profile.Rooms.getOwnedRoomIds(); if (ids.length == 0) { rs.json([]); return; } const roomFactoriesPreInit = ids.map(id => new RoomFactory({ id: id })); const roomFactories = (await Promise.all(roomFactoriesPreInit.map(factory => factory.init()))).filter(val => val !== null); const detailsPromises = (await Promise.all(roomFactories.map(factory => factory.export()))); rs.json(detailsPromises.map(roomDetails => roomDetails.Room)); }, ); route.router.get('/v1/hot', APIUtils.Authentication, APIUtils.AuthenticationType(AuthType.Game), 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 == RoomDataTypes.RoomAccessibility.Public)); }, ); route.router.get('/v2/baserooms', APIUtils.Authentication, APIUtils.AuthenticationType(AuthType.Game), async (_rq, rs) => { const rooms = await Rooms.getAllBuiltinRoomGenerations(); rs.json(rooms.map(room => room.Room).filter(room => room.CloningAllowed)); }, ); interface GetRoomByNameParams { name?: string } route.router.get('/v2/name/:name', APIUtils.Authentication, APIUtils.AuthenticationType(AuthType.Game), async (rq: express.Request, rs: express.Response) => { if (!rq.params.name) { rs.sendStatus(400); return; } const room = await Rooms.getByName(rq.params.name.trim()); if (room) { rs.json(room.Room); return; } else if (rq.params.name == 'DormRoom') { const dorm = await Rooms.getProfileDormDefault(rs.locals.profile); if (dorm) rs.json(dorm.Room); else rs.sendStatus(404); return; } else { rs.sendStatus(404); return; } }, ); route.router.post('/v1/roomRolePermissions', APIUtils.Authentication, APIUtils.AuthenticationType(AuthType.Game), (_rq, rs) => { rs.sendStatus(200); }, ); route.router.get('/v1/agRoomIds', APIUtils.Authentication, APIUtils.AuthenticationType(AuthType.Game), async (_rq, rs) => { const rooms = await Rooms.getAllBuiltinRoomGenerations(); rs.json(rooms.map(det => det.Room.RoomId)); }, ); const CloneRoomSchema = z.object({ Name: z.string(), RoomId: z.number() }); interface CloneRoomBody { Name: string, RoomId: number } route.router.post('/v1/clone', APIUtils.Authentication, APIUtils.AuthenticationType(AuthType.Game), express.json(), APIUtils.validateRequestBody(CloneRoomSchema), async (rq: express.Request, rs: express.Response) => { const room = await Rooms.cloneRoom(rq.body.RoomId, rq.body.Name, rs.locals.profile); const masterRoomFactory = await new RoomFactory({ id: rq.body.RoomId }).init(); rs.json({ Result: room.result, RoomDetails: room.result == RoomDataTypes.CreateModifyRoomStatus.Success ? await room.factory?.export() : await masterRoomFactory?.export() }); if ( room.result == RoomDataTypes.CreateModifyRoomStatus.Success && room.factory ) rs.locals.profile.Rooms.addOwnedRoomId(room.factory.RoomId); }, ); const CreatorActionContextScheme = z.object({ IsTeachableMomentRunning: z.boolean() }); const SaveDataScheme = z.object({ RoomSceneId: z.number(), RoomDataFilename: z.string().min(6).max(128), InventionUsages: z.array(z.number()), CreatorActionContext: CreatorActionContextScheme, RequestPlayerId: z.number() }); interface CreatorActionContextBody { IsTeachableMomentRunning: boolean } interface SaveDataBody { CreatorActionContext: CreatorActionContextBody, InventionUsages: number[], RequestPlayerId: number, RoomDataFilename: string, RoomSceneId: number } route.router.post('/v4/saveData', APIUtils.Authentication, APIUtils.AuthenticationType(AuthType.Game), express.json(), APIUtils.validateRequestBody(SaveDataScheme), async (rq: express.Request, rs: express.Response) => { log.d(`Request to save: '${rq.body.RoomDataFilename}'`); const currentInstance = rs.locals.profile.getInstance(); if (!currentInstance) { rs.status(400).json(APIUtils.genericResponseFormat(true, "Player not currently in a room")); return; } const subroomFactory = await new SubroomFactory({ roomId: currentInstance.roomId, subroomId: rq.body.RoomSceneId, factoryMode: FactoryMode.Write, writeMode: WriteMode.Overwrite }).init(); const splitFilename = rq.body.RoomDataFilename.split('/'); const newFilename = splitFilename[splitFilename.length - 1]; if (!newFilename) { rs.sendStatus(400); log.e(`New filename was invalid: '${newFilename}'`); } else { subroomFactory.DataBlobName = newFilename; subroomFactory.addBlobHistory(new Date(), newFilename); await subroomFactory.write(); rs.json(subroomFactory.export()); currentInstance.dataBlob = newFilename; currentInstance.updatePlayers(); Rooms.socketUpdateRoom(currentInstance); } }, );