All checks were successful
Galvanic Corrosion Cross-Compile / build (push) Successful in 49s
* Added Storage and room saving (will be moved to events later)
* Moved `UnifiedProfile` to new `Server` object, along with `CDN`
- Will move `Rooms` and others to this later
259 lines
7.6 KiB
TypeScript
259 lines
7.6 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 { 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<Params>, 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<GetRoomByNameParams>, 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<NoBody, NoBody, CloneRoomBody>, 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<NoBody, NoBody, SaveDataBody>, 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);
|
|
}
|
|
},
|
|
|
|
); |