roooooms 2
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
"@felix/bcrypt": "jsr:@felix/bcrypt@^1.0.5",
|
"@felix/bcrypt": "jsr:@felix/bcrypt@^1.0.5",
|
||||||
"@hono/hono": "jsr:@hono/hono@^4.8.5",
|
"@hono/hono": "jsr:@hono/hono@^4.8.5",
|
||||||
"@hono/zod-validator": "jsr:@hono/zod-validator@^0.7.2",
|
"@hono/zod-validator": "jsr:@hono/zod-validator@^0.7.2",
|
||||||
|
"@oneday/http-status": "jsr:@oneday/http-status@^0.2.0",
|
||||||
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.5.0",
|
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.5.0",
|
||||||
"@std/assert": "jsr:@std/assert@1",
|
"@std/assert": "jsr:@std/assert@1",
|
||||||
"sharp": "npm:sharp@^0.34.3",
|
"sharp": "npm:sharp@^0.34.3",
|
||||||
|
|||||||
@@ -1272,7 +1272,7 @@
|
|||||||
"Name": "RecCenter",
|
"Name": "RecCenter",
|
||||||
"ReplicationId": "02ed2947-2db9-62c4-49b0-76d70fd432bb",
|
"ReplicationId": "02ed2947-2db9-62c4-49b0-76d70fd432bb",
|
||||||
"Description": "A social hub to meet and mingle with friends new and old.",
|
"Description": "A social hub to meet and mingle with friends new and old.",
|
||||||
"Accessibility": 2,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": false,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
@@ -1303,7 +1303,7 @@
|
|||||||
"Description": "Take turns drawing, acting, and guessing funny phrases with your friends!",
|
"Description": "Take turns drawing, acting, and guessing funny phrases with your friends!",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": false,
|
"SupportsScreens": false,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1332,7 +1332,7 @@
|
|||||||
"Description": "A leisurely stroll through the grass. Throw your disc into the goal. Sounds easy, right?",
|
"Description": "A leisurely stroll through the grass. Throw your disc into the goal. Sounds easy, right?",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1361,7 +1361,7 @@
|
|||||||
"Description": "Throw your disc through hazards and around wind machines on this challenging course!",
|
"Description": "Throw your disc through hazards and around wind machines on this challenging course!",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1390,7 +1390,7 @@
|
|||||||
"Description": "Throw dodgeballs to knock out your friends in this gym classic!",
|
"Description": "Throw dodgeballs to knock out your friends in this gym classic!",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1419,7 +1419,7 @@
|
|||||||
"Description": "A simple rally game between two players in a plexiglass tube with a zero-g ball.",
|
"Description": "A simple rally game between two players in a plexiglass tube with a zero-g ball.",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": false,
|
"SupportsScreens": false,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1448,7 +1448,7 @@
|
|||||||
"Description": "Red and Blue teams splat each other in capture the flag and team battle.",
|
"Description": "Red and Blue teams splat each other in capture the flag and team battle.",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": true,
|
"SupportsLevelVoting": true,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1542,7 +1542,7 @@
|
|||||||
"Description": "Red and Blue teams splat each other in capture the flag and team battle.",
|
"Description": "Red and Blue teams splat each other in capture the flag and team battle.",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": true,
|
"SupportsLevelVoting": true,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": false,
|
"SupportsScreens": false,
|
||||||
"SupportsWalkVR": false,
|
"SupportsWalkVR": false,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1623,7 +1623,7 @@
|
|||||||
"Description": "The goblin king stole Coach's Golden Trophy. Team up and embark on an epic quest to recover it!",
|
"Description": "The goblin king stole Coach's Golden Trophy. Team up and embark on an epic quest to recover it!",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1652,7 +1652,7 @@
|
|||||||
"Description": "Robot invaders threaten the galaxy! Team up with your friends and bring the laser heat!",
|
"Description": "Robot invaders threaten the galaxy! Team up with your friends and bring the laser heat!",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1681,7 +1681,7 @@
|
|||||||
"Description": "Can your band of adventurers brave the enchanted wilds, and lift the curse of the crimson cauldron?",
|
"Description": "Can your band of adventurers brave the enchanted wilds, and lift the curse of the crimson cauldron?",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1710,7 +1710,7 @@
|
|||||||
"Description": "Can your pirate crew get to the Isle, defeat its fearsome guardian, and escape with the gold?",
|
"Description": "Can your pirate crew get to the Isle, defeat its fearsome guardian, and escape with the gold?",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1739,7 +1739,7 @@
|
|||||||
"Description": "Teams of three run around slamming themselves into an over-sized soccer ball. Goal!",
|
"Description": "Teams of three run around slamming themselves into an over-sized soccer ball. Goal!",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1768,7 +1768,7 @@
|
|||||||
"Description": "Teams battle each other and waves of robots.",
|
"Description": "Teams battle each other and waves of robots.",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": true,
|
"SupportsLevelVoting": true,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -1924,7 +1924,7 @@
|
|||||||
"Name": "PerformanceHall",
|
"Name": "PerformanceHall",
|
||||||
"ReplicationId": "22ab0d3c-3d7d-70e4-eb5c-c8c47cca1906",
|
"ReplicationId": "22ab0d3c-3d7d-70e4-eb5c-c8c47cca1906",
|
||||||
"Description": "A theater for plays, music, comedy and other performances.",
|
"Description": "A theater for plays, music, comedy and other performances.",
|
||||||
"Accessibility": 2,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": true,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
@@ -2361,7 +2361,7 @@
|
|||||||
"Description": "Throw dodgeballs to knock out your friends in this gym classic!",
|
"Description": "Throw dodgeballs to knock out your friends in this gym classic!",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": false,
|
"SupportsScreens": false,
|
||||||
"SupportsWalkVR": false,
|
"SupportsWalkVR": false,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -2390,7 +2390,7 @@
|
|||||||
"Description": "Gather your vampire hunting crew, conquer a legendary castle, and restore peace to Rec Room!",
|
"Description": "Gather your vampire hunting crew, conquer a legendary castle, and restore peace to Rec Room!",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
@@ -2419,7 +2419,7 @@
|
|||||||
"Description": "Hang out with friends, bowl a few games, eat snacks!",
|
"Description": "Hang out with friends, bowl a few games, eat snacks!",
|
||||||
"Accessibility": 1,
|
"Accessibility": 1,
|
||||||
"SupportsLevelVoting": false,
|
"SupportsLevelVoting": false,
|
||||||
"CloningAllowed": true,
|
"CloningAllowed": false,
|
||||||
"SupportsScreens": true,
|
"SupportsScreens": true,
|
||||||
"SupportsWalkVR": true,
|
"SupportsWalkVR": true,
|
||||||
"SupportsTeleportVR": true,
|
"SupportsTeleportVR": true,
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import { PushNotificationId } from "./server/socket/signalr/types.ts";
|
|||||||
import { genericResponse } from "./util/api.ts";
|
import { genericResponse } from "./util/api.ts";
|
||||||
import { getNetConfig } from "./net.ts";
|
import { getNetConfig } from "./net.ts";
|
||||||
import { TokenFormat, TokenType } from "./server/platforms/types.ts";
|
import { TokenFormat, TokenType } from "./server/platforms/types.ts";
|
||||||
import { Context } from "node:vm";
|
import { HonoEnv } from "./util/types.ts";
|
||||||
|
import { Context } from "@hono/hono";
|
||||||
|
import { compress } from "@hono/hono/compress";
|
||||||
|
|
||||||
LoggingConfiguration.resetTimeFormat = TimeFormat.Unix;
|
LoggingConfiguration.resetTimeFormat = TimeFormat.Unix;
|
||||||
LoggingConfiguration.resetLogTiming = LogTiming.Microtask;
|
LoggingConfiguration.resetLogTiming = LogTiming.Microtask;
|
||||||
@@ -48,8 +50,10 @@ await routeImporter(AppRoot.app, 'src/', [
|
|||||||
'routes/img'
|
'routes/img'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
AppRoot.app.use(compress());
|
||||||
|
|
||||||
// deno-lint-ignore require-await
|
// deno-lint-ignore require-await
|
||||||
AppRoot.app.use('*', async (c: Context) => {
|
AppRoot.app.use('*', async (c: Context<HonoEnv>) => {
|
||||||
return c.json(genericResponse(false, "Resource Not Found"), 404);
|
return c.json(genericResponse(false, "Resource Not Found"), 404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
26
src/routes/api/routes/rooms.ts
Normal file
26
src/routes/api/routes/rooms.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import z from "zod";
|
||||||
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
|
import { typedZValidator } from "../../../util/validators.ts";
|
||||||
|
import Server from "../../../server/server.ts";
|
||||||
|
|
||||||
|
export const route = createHonoRoute("/rooms");
|
||||||
|
|
||||||
|
const getRoomDetailsParamSchema = z.object({
|
||||||
|
id: z.coerce.number().max(Math.pow(2, 31))
|
||||||
|
});
|
||||||
|
route.app.get('/v4/details/:id', typedZValidator('param', getRoomDetailsParamSchema), async (c, nxt) => {
|
||||||
|
const room = await Server.Rooms.get(parseInt(c.req.param('id')));
|
||||||
|
if (room === null) return await nxt();
|
||||||
|
else return c.json(await room.export());
|
||||||
|
});
|
||||||
|
const getRoomByNameParamSchema = z.object({
|
||||||
|
name: z.string()
|
||||||
|
});
|
||||||
|
route.app.get('/v2/name/:name', typedZValidator('param', getRoomByNameParamSchema), async (c, nxt) => {
|
||||||
|
const id = await Server.Rooms.getIdFromName(c.req.param('name'));
|
||||||
|
if (id == null) return await nxt();
|
||||||
|
|
||||||
|
const room = await Server.Rooms.get(id);
|
||||||
|
if (room == null) return await nxt();
|
||||||
|
else return c.json((await room.export()).Room);
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { authenticate, genericResponse } from "../../../util/api.ts";
|
import { authenticate } from "../../../util/api.ts";
|
||||||
import { createHonoRoute } from "../../../util/import.ts";
|
import { createHonoRoute } from "../../../util/import.ts";
|
||||||
import { typedZValidator } from "../../../util/validators.ts";
|
import { typedZValidator } from "../../../util/validators.ts";
|
||||||
import { HonoEnv } from "../../../util/types.ts";
|
import { HonoEnv } from "../../../util/types.ts";
|
||||||
@@ -23,7 +23,6 @@ const settingsSetSchema = z.object({
|
|||||||
});
|
});
|
||||||
route.app.post('/v2/set', typedZValidator('json', settingsSetSchema), async c => {
|
route.app.post('/v2/set', typedZValidator('json', settingsSetSchema), async c => {
|
||||||
const { Key, Value } = c.req.valid('json');
|
const { Key, Value } = c.req.valid('json');
|
||||||
if (!Key) return c.json(genericResponse(false, "Internal Server Error"), 500);
|
|
||||||
|
|
||||||
await c.get('profile').Settings.setSetting(Key, Value);
|
await c.get('profile').Settings.setSetting(Key, Value);
|
||||||
return c.status(200);
|
return c.status(200);
|
||||||
|
|||||||
@@ -2,39 +2,50 @@ import z from "zod";
|
|||||||
import Server from "../../server/server.ts";
|
import Server from "../../server/server.ts";
|
||||||
import { createHonoRoute } from "../../util/import.ts";
|
import { createHonoRoute } from "../../util/import.ts";
|
||||||
import { typedZValidator } from "../../util/validators.ts";
|
import { typedZValidator } from "../../util/validators.ts";
|
||||||
import { FileType, File } from "../../server/content/base.ts";
|
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { RootPath } from "../../util/path.ts";
|
import { RootPath } from "../../util/path.ts";
|
||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
|
import { statusResponse } from "../../util/api.ts";
|
||||||
|
import { HTTPStatus } from "@oneday/http-status";
|
||||||
|
import { Buffer } from "node:buffer";
|
||||||
|
|
||||||
export const route = createHonoRoute('/img');
|
export const route = createHonoRoute('/img');
|
||||||
|
|
||||||
async function convertImage(query: {cropSquare?: boolean | undefined;width?: number | undefined;height?: number | undefined;}, data: Uint8Array<ArrayBufferLike>): Promise<Uint8Array<ArrayBufferLike>> {
|
const log = new Logging("ImageRoute");
|
||||||
const image = sharp(data);
|
|
||||||
const rootMetadata = await image.metadata();
|
|
||||||
const squareSize = Math.min(rootMetadata.width, rootMetadata.height);
|
|
||||||
if (query.cropSquare) image.resize(squareSize, squareSize);
|
|
||||||
|
|
||||||
const newImage = sharp(await image.png().toBuffer());
|
async function convertImage(query: ImageQuery, data: Uint8Array<ArrayBufferLike>): Promise<Uint8Array<ArrayBufferLike> | null> {
|
||||||
if (query.width && query.height)
|
try {
|
||||||
newImage.resize(query.width, query.height);
|
const image = sharp(data);
|
||||||
else if (query.width)
|
const rootMetadata = await image.metadata();
|
||||||
newImage.resize(query.width);
|
const squareSize = Math.min(rootMetadata.width, rootMetadata.height);
|
||||||
else if (query.height)
|
if (query.cropSquare) image.resize(squareSize, squareSize);
|
||||||
newImage.resize(undefined, query.height);
|
|
||||||
|
|
||||||
return await newImage.png().toBuffer();
|
const newImage = sharp(await image.png().toBuffer());
|
||||||
|
if (query.width && query.height)
|
||||||
|
newImage.resize(query.width, query.height);
|
||||||
|
else if (query.width)
|
||||||
|
newImage.resize(query.width);
|
||||||
|
else if (query.height)
|
||||||
|
newImage.resize(undefined, query.height);
|
||||||
|
|
||||||
|
return await newImage.png().toBuffer();
|
||||||
|
} catch (err) {
|
||||||
|
log.w(`Image transformation failed: ${(err as Error).stack}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const imgNameParamSchema = z.object({
|
const imgNameParamSchema = z.object({
|
||||||
imgName: z.string().min(4).includes('.')
|
imgName: z.string().min(5).includes('.')
|
||||||
});
|
});
|
||||||
const imgQuerySchema = z.object({
|
const imgQuerySchema = z.object({
|
||||||
cropSquare: z.coerce.boolean().optional(),
|
cropSquare: z.coerce.boolean().optional(),
|
||||||
width: z.coerce.number().min(64).max(3840).optional(),
|
width: z.coerce.number().min(64).max(3840).optional(),
|
||||||
height: z.coerce.number().min(64).max(2160).optional(),
|
height: z.coerce.number().min(64).max(2160).optional(),
|
||||||
});
|
});
|
||||||
|
type ImageQuery = z.infer<typeof imgQuerySchema>;
|
||||||
|
|
||||||
route.app.get('/:imgName',
|
route.app.get('/:imgName',
|
||||||
typedZValidator('param', imgNameParamSchema),
|
typedZValidator('param', imgNameParamSchema),
|
||||||
typedZValidator('query', imgQuerySchema),
|
typedZValidator('query', imgQuerySchema),
|
||||||
@@ -43,29 +54,29 @@ route.app.get('/:imgName',
|
|||||||
const { imgName } = c.req.valid('param');
|
const { imgName } = c.req.valid('param');
|
||||||
const query = c.req.valid('query');
|
const query = c.req.valid('query');
|
||||||
|
|
||||||
const file: File | null = await Server.Content.getFile(`img/${imgName}`);
|
const datas: Uint8Array<ArrayBufferLike>[] = (await Promise.all<Uint8Array<ArrayBufferLike> | null>([
|
||||||
let raw: Uint8Array<ArrayBuffer> | null = null;
|
new Promise(resolve => {
|
||||||
if (!file) {
|
Deno.readFile(path.join(RootPath, "res/baseimg/", imgName)).then(img => {
|
||||||
try {
|
resolve(img);
|
||||||
raw = await Deno.readFile(path.join(RootPath, "res/baseimg/", imgName));
|
}).catch(() => {
|
||||||
} catch {
|
resolve(null);
|
||||||
raw = null;
|
});
|
||||||
}
|
}),
|
||||||
}
|
new Promise(resolve => {
|
||||||
|
Server.Content.getFile(`img/${imgName}`).then(file => {
|
||||||
if (!raw && file && file.Meta.Type !== FileType.Image) return c.status(404);
|
if (file) resolve(file.Data);
|
||||||
|
else resolve(null);
|
||||||
try {
|
}).catch(() => {
|
||||||
let result: Uint8Array<ArrayBufferLike> | null = null;
|
resolve(null);
|
||||||
if (file) result = await convertImage(query, file.Data);
|
});
|
||||||
else if (raw) result = await convertImage(query, raw);
|
})
|
||||||
|
])).filter(val => val !== null);
|
||||||
if (result) return c.body(result, 200, { "Cache-Control": "public, no-transform, max-age=1800", "Content-Type": "image/png" });
|
|
||||||
else return c.status(404);
|
if (datas.length == 0) return statusResponse(c, HTTPStatus.NotFound);
|
||||||
|
else {
|
||||||
} catch (err) {
|
const result = await convertImage(query, datas[0]);
|
||||||
new Logging("ImageRoute").w(`Sharp error: ${err}`);
|
if (result == null) return statusResponse(c, HTTPStatus.InternalServerError, "Image transformation failed. Contact an administrator.");
|
||||||
return c.status(500);
|
return c.body(Buffer.from(result), 200, { "Cache-Control": "public, no-transform, max-age=1800", "Content-Type": "image/png" });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export class Instance {
|
|||||||
getPlayers() {
|
getPlayers() {
|
||||||
return this.#players.values().toArray();
|
return this.#players.values().toArray();
|
||||||
}
|
}
|
||||||
playerIsHere(profile: Profile) {
|
hasPlayer(profile: Profile) {
|
||||||
return Boolean(this.getPlayers().find(prof => prof.same(profile)));
|
return this.#players.values().some(prof => prof.getId() === profile.getId());
|
||||||
}
|
}
|
||||||
removePlayer(profile: Profile) {
|
removePlayer(profile: Profile) {
|
||||||
this.#players.delete(profile);
|
this.#players.delete(profile);
|
||||||
|
|||||||
@@ -3,19 +3,146 @@ import { ServerContentBase } from "../ContentBase.ts";
|
|||||||
import KV from "../persistence/kv.ts";
|
import KV from "../persistence/kv.ts";
|
||||||
import type Profile from "../profiles/profile.ts";
|
import type Profile from "../profiles/profile.ts";
|
||||||
import { RoomFactory } from "./internal/RoomFactory.ts";
|
import { RoomFactory } from "./internal/RoomFactory.ts";
|
||||||
import { FactoryMode } from "./internal/RoomDataTypes.ts";
|
import { FactoryMode, HardwareSupports, RoomDataTypes, WriteMode } from "./internal/RoomDataTypes.ts";
|
||||||
|
import { AGRoom, AGRoomLocation, AGRoomRuntimeConfig } from "./internal/ClientRoomTypes.ts";
|
||||||
|
import Command from "../commands/command.ts";
|
||||||
|
import z from "zod";
|
||||||
|
import { RoomLocation } from "../instances/base.ts";
|
||||||
|
|
||||||
export class ServerRoomsBase extends ServerContentBase {
|
export class ServerRoomsBase extends ServerContentBase {
|
||||||
|
|
||||||
#subroomKv = new KV('subrooms', true);
|
#subroomKv = new KV('subrooms', true);
|
||||||
#log = new Logging("Rooms");
|
#log = new Logging("Rooms");
|
||||||
|
|
||||||
|
static agRoomIdsKey = "agrooms";
|
||||||
|
static baseRoomIdsKey = "baserooms";
|
||||||
|
|
||||||
static roomNamesKey = "room_names";
|
static roomNamesKey = "room_names";
|
||||||
static playerDormsKey = "dorms";
|
static playerDormsKey = "dorms";
|
||||||
|
|
||||||
protected override async start() {
|
#agrooms: Set<number> = new Set();
|
||||||
|
#baserooms: Set<number> = new Set();
|
||||||
|
|
||||||
|
override async start() {
|
||||||
await this.#subroomKv.init();
|
await this.#subroomKv.init();
|
||||||
this.#log.i('[sub]rooms database initialized');
|
this.#log.i('[sub]rooms database initialized');
|
||||||
|
|
||||||
|
const agrooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.agRoomIdsKey]);
|
||||||
|
if (agrooms.value !== null) this.#agrooms = agrooms.value;
|
||||||
|
this.#log.i(`${this.#agrooms.size} AG rooms exist`);
|
||||||
|
|
||||||
|
const baserooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.baseRoomIdsKey]);
|
||||||
|
if (baserooms.value !== null) this.#baserooms = baserooms.value;
|
||||||
|
|
||||||
|
this.server.Commands.addRootCommand(new Command({
|
||||||
|
key: ["rooms", "r", "room"],
|
||||||
|
subcommands: [
|
||||||
|
new Command({
|
||||||
|
key: ["initag", "initagrooms", "initagroom", "iag"],
|
||||||
|
zod: z.tuple([]).rest(z.string()),
|
||||||
|
exec: async (...arrayPath: string[]) => {
|
||||||
|
const path = arrayPath.join(' ');
|
||||||
|
try {
|
||||||
|
const config = JSON.parse((await Deno.readTextFile(path)).toString()) as AGRoomRuntimeConfig;
|
||||||
|
this.#log.d('Starting AG room initialization');
|
||||||
|
this.initBuiltinRooms(config.Rooms, config.Locations);
|
||||||
|
} catch (err) {
|
||||||
|
return err as Error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
help: "Initialize AG rooms with AGRoomRuntimeConfig from provided file path"
|
||||||
|
}),
|
||||||
|
new Command({
|
||||||
|
key: ["getplayerdorm", "playerdorm", "pd", "dorm"],
|
||||||
|
zod: z.tuple([z.coerce.number().min(1).max(Math.pow(2, 31))]),
|
||||||
|
exec: async (playerId: number) => {
|
||||||
|
const factory = await this.getPlayerDorm(this.server.Profiles.get(playerId))
|
||||||
|
},
|
||||||
|
help: "Get the domroom information for a certain profile/player"
|
||||||
|
})
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
async #writeAgRooms() {
|
||||||
|
await this.kv.getKv().set([ServerRoomsBase.agRoomIdsKey], this.#agrooms);
|
||||||
|
}
|
||||||
|
async #writeBaseRooms() {
|
||||||
|
await this.kv.getKv().set([ServerRoomsBase.baseRoomIdsKey], this.#baserooms);
|
||||||
|
}
|
||||||
|
|
||||||
|
async initBuiltinRooms(rooms: AGRoom[], locations: AGRoomLocation[]) {
|
||||||
|
await Promise.all(rooms.map(async room => {
|
||||||
|
if (room.Accessibility == RoomDataTypes.RoomAccessibility.Private) return;
|
||||||
|
if ([
|
||||||
|
"ArtTesting",
|
||||||
|
"AnimationRecordingStudio",
|
||||||
|
"Calibration",
|
||||||
|
"ARRoom",
|
||||||
|
"Registration",
|
||||||
|
"DormRoom"
|
||||||
|
].includes(room.Name)) return;
|
||||||
|
|
||||||
|
const roomFactory = await this.write();
|
||||||
|
if (roomFactory == null) {
|
||||||
|
this.#log.w(`No factory given while writing builtin room "${room.Name}"!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportsVRLow = room.Scenes.map(scene => locations.find(loc => loc.ReplicationId == scene.RoomSceneLocationId))
|
||||||
|
.filter(val => typeof val !== 'undefined').some(loc => loc.SupportsVRLow) ?? false;
|
||||||
|
const supportsMobile = room.Scenes.map(scene => locations.find(loc => loc.ReplicationId == scene.RoomSceneLocationId))
|
||||||
|
.filter(val => typeof val !== 'undefined').some(loc => loc.SupportsMobile) ?? false;
|
||||||
|
|
||||||
|
roomFactory.Name = room.Name;
|
||||||
|
roomFactory.Description = room.Description;
|
||||||
|
roomFactory.Accessibility = room.Accessibility;
|
||||||
|
roomFactory.State = RoomDataTypes.RoomState.Active;
|
||||||
|
roomFactory.Description = room.Description;
|
||||||
|
roomFactory.IsAGRoom = true;
|
||||||
|
roomFactory.CloningAllowed = room.CloningAllowed;
|
||||||
|
roomFactory.ImageName = `${room.Name}.png`
|
||||||
|
|
||||||
|
const supportPromises: Promise<unknown>[] = [];
|
||||||
|
roomFactory.removeAllHardwareSupport();
|
||||||
|
if (room.SupportsScreens) supportPromises.push(roomFactory.addHardwareSupport("screens"));
|
||||||
|
if (room.SupportsWalkVR) supportPromises.push(roomFactory.addHardwareSupport("walk_vr"));
|
||||||
|
if (room.SupportsTeleportVR) supportPromises.push(roomFactory.addHardwareSupport("teleport_vr"));
|
||||||
|
if (supportsMobile) supportPromises.push(roomFactory.addHardwareSupport("mobile"));
|
||||||
|
if (supportsVRLow) supportPromises.push(roomFactory.addHardwareSupport("low_vr"));
|
||||||
|
|
||||||
|
await Promise.all(room.Scenes.map(async scene => {
|
||||||
|
const subroomFactory = await roomFactory.newSubroom();
|
||||||
|
subroomFactory.addSave("");
|
||||||
|
|
||||||
|
subroomFactory.RoomId = roomFactory.getRoomId();
|
||||||
|
subroomFactory.Name = scene.Name;
|
||||||
|
subroomFactory.RoomSceneLocationId = scene.RoomSceneLocationId;
|
||||||
|
subroomFactory.IsSandbox = scene.IsSandbox;
|
||||||
|
subroomFactory.CanMatchmakeInto = scene.CanMatchmakeInto;
|
||||||
|
subroomFactory.MaxPlayers = scene.MaxPlayers;
|
||||||
|
|
||||||
|
await subroomFactory.write();
|
||||||
|
|
||||||
|
roomFactory.addSubroom(subroomFactory.RoomSceneId);
|
||||||
|
}));
|
||||||
|
|
||||||
|
await Promise.all(supportPromises);
|
||||||
|
|
||||||
|
this.#agrooms.add(roomFactory.getRoomId());
|
||||||
|
await roomFactory.write();
|
||||||
|
|
||||||
|
if (room.CloningAllowed) this.#baserooms.add(roomFactory.getRoomId());
|
||||||
|
}));
|
||||||
|
|
||||||
|
await this.#writeAgRooms();
|
||||||
|
await this.#writeBaseRooms();
|
||||||
|
this.#log.i(`${this.#agrooms.size} AG rooms added: [${this.#agrooms.values().toArray().join(',')}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAvailableRoomId() {
|
||||||
|
let id = Math.round(Math.random() * Math.pow(2, 31));
|
||||||
|
while ((await this.kv.getKv().get<unknown>([RoomFactory.roomsKey, id, 'meta'])).value !== null) id = await this.getAvailableRoomId();
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
getKv() {
|
getKv() {
|
||||||
@@ -30,11 +157,48 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
|
|
||||||
async getPlayerDorm(profile: Profile) {
|
async getPlayerDorm(profile: Profile) {
|
||||||
const id = await this.kv.getKv().get<number>([ServerRoomsBase.playerDormsKey, profile.getId()]);
|
const id = await this.kv.getKv().get<number>([ServerRoomsBase.playerDormsKey, profile.getId()]);
|
||||||
if (id.value == null) return null;
|
if (id.value == null) {
|
||||||
|
const roomFactory = await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree, id: await this.getAvailableRoomId() });
|
||||||
|
if (!roomFactory) return null;
|
||||||
|
roomFactory.setRoomProperties({
|
||||||
|
Name: `DormRoom`,
|
||||||
|
Description: "Your private dorm.",
|
||||||
|
CreatorPlayerId: profile.getId(),
|
||||||
|
ImageName: "",
|
||||||
|
State: RoomDataTypes.RoomState.Active,
|
||||||
|
Accessibility: RoomDataTypes.RoomAccessibility.Private,
|
||||||
|
SupportsLevelVoting: false,
|
||||||
|
IsAGRoom: false,
|
||||||
|
IsDormRoom: true,
|
||||||
|
CloningAllowed: false,
|
||||||
|
AllowsJuniors: true,
|
||||||
|
RoomWarningMask: 0,
|
||||||
|
CustomRoomWarning: "",
|
||||||
|
DisableMicAutoMute: null
|
||||||
|
});
|
||||||
|
|
||||||
|
await roomFactory.addHardwareSupport(...HardwareSupports);
|
||||||
|
roomFactory.setTags(new Set(["dormroom"]));
|
||||||
|
|
||||||
|
const subroomFactory = await roomFactory.newSubroom();
|
||||||
|
subroomFactory.CanMatchmakeInto = true;
|
||||||
|
subroomFactory.IsSandbox = true;
|
||||||
|
subroomFactory.MaxPlayers = 4;
|
||||||
|
subroomFactory.Name = "Home";
|
||||||
|
subroomFactory.RoomId = roomFactory.getRoomId();
|
||||||
|
subroomFactory.RoomSceneLocationId = RoomLocation.DormRoom;
|
||||||
|
subroomFactory.addSave("");
|
||||||
|
roomFactory.addSubroom(subroomFactory.RoomSceneId);
|
||||||
|
|
||||||
|
await subroomFactory.write();
|
||||||
|
await roomFactory.write();
|
||||||
|
|
||||||
|
return roomFactory;
|
||||||
|
}
|
||||||
else return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Fetch, id: id.value });
|
else return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Fetch, id: id.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFromName(name: string) {
|
async getByName(name: string) {
|
||||||
const id = await this.getIdFromName(name);
|
const id = await this.getIdFromName(name);
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
return await this.get(id);
|
return await this.get(id);
|
||||||
@@ -44,4 +208,8 @@ export class ServerRoomsBase extends ServerContentBase {
|
|||||||
return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Fetch, id: id });
|
return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Fetch, id: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async write(mode?: WriteMode) {
|
||||||
|
return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: await this.getAvailableRoomId() });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -55,4 +55,9 @@ export interface AGRoomLocation {
|
|||||||
}
|
}
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AGRoomRuntimeConfig {
|
||||||
|
Locations: AGRoomLocation[],
|
||||||
|
Rooms: AGRoom[]
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,9 @@ export enum FactoryMode {
|
|||||||
Write = 'write'
|
Write = 'write'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All clients RecRoom can run on.
|
||||||
|
*/
|
||||||
export const HardwareSupports = ["screens", "walk_vr", "teleport_vr", "low_vr", "mobile"] as const;
|
export const HardwareSupports = ["screens", "walk_vr", "teleport_vr", "low_vr", "mobile"] as const;
|
||||||
export type HardwareSupport = typeof HardwareSupports[number];
|
export type HardwareSupport = typeof HardwareSupports[number];
|
||||||
|
|
||||||
@@ -138,8 +141,7 @@ export interface DatabaseRoomContent {
|
|||||||
}
|
}
|
||||||
export interface DatabaseRoom {
|
export interface DatabaseRoom {
|
||||||
Hardware: Set<HardwareSupport>,
|
Hardware: Set<HardwareSupport>,
|
||||||
Subrooms: number[],
|
Tags: Set<string>,
|
||||||
Tags: Set<number>,
|
|
||||||
CoOwners: Set<number>,
|
CoOwners: Set<number>,
|
||||||
InvitedCoOwners: Set<number>,
|
InvitedCoOwners: Set<number>,
|
||||||
Hosts: Set<number>,
|
Hosts: Set<number>,
|
||||||
@@ -154,14 +156,13 @@ export interface DatabaseSubroom {
|
|||||||
IsSandbox: boolean,
|
IsSandbox: boolean,
|
||||||
MaxPlayers: number,
|
MaxPlayers: number,
|
||||||
CanMatchmakeInto: boolean,
|
CanMatchmakeInto: boolean,
|
||||||
LatestSaveId: number,
|
LatestSaveId: number | null,
|
||||||
Saves: RoomSave[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RoomSaveMap = Map<number, RoomSave>
|
||||||
export interface RoomSave {
|
export interface RoomSave {
|
||||||
SaveId: number,
|
|
||||||
DataBlobName: string,
|
DataBlobName: string,
|
||||||
SavedAt: string
|
SavedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export * as RoomDataTypes from "./RoomDataTypes.ts";
|
export * as RoomDataTypes from "./RoomDataTypes.ts";
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import type KV from "../../persistence/kv.ts";
|
import type KV from "../../persistence/kv.ts";
|
||||||
import { type ServerBase } from "../../server.ts";
|
import { type ServerBase } from "../../server.ts";
|
||||||
import { DatabaseRoom, FactoryMode, GalvanicTagDTO, RoomDataTypes, TagDTO, TagType, WriteMode } from "./RoomDataTypes.ts";
|
import { DatabaseRoom, FactoryMode, GalvanicTagDTO, HardwareSupports, RoomAccessibility, RoomDataTypes, RoomState, TagDTO, TagType, WriteMode } from "./RoomDataTypes.ts";
|
||||||
import { SubroomFactory } from "./SubroomFactory.ts";
|
import { SubroomFactory } from "./SubroomFactory.ts";
|
||||||
import { ServerRoomsBase } from "../base.ts";
|
import { ServerRoomsBase } from "../base.ts";
|
||||||
|
|
||||||
const log = new Logging("RoomFactory");
|
const log = new Logging("RoomFactory");
|
||||||
|
|
||||||
const roomDebug = true;
|
const roomDebug = false;
|
||||||
|
|
||||||
interface FactoryOptionsBase {
|
interface FactoryOptionsBase {
|
||||||
mode: FactoryMode,
|
mode: FactoryMode,
|
||||||
id?: number,
|
id: number,
|
||||||
name?: string
|
name?: string
|
||||||
}
|
}
|
||||||
export interface WriteFactoryOptions extends FactoryOptionsBase {
|
export interface WriteFactoryOptions extends FactoryOptionsBase {
|
||||||
@@ -36,6 +36,7 @@ export class RoomFactory {
|
|||||||
writeMode: WriteMode = WriteMode.WriteIfFree;
|
writeMode: WriteMode = WriteMode.WriteIfFree;
|
||||||
|
|
||||||
#obj: DatabaseRoom | null = null;
|
#obj: DatabaseRoom | null = null;
|
||||||
|
#subrooms: Set<number> | null = null;
|
||||||
#hardwareSupport: Set<RoomDataTypes.HardwareSupport> | null = null;
|
#hardwareSupport: Set<RoomDataTypes.HardwareSupport> | null = null;
|
||||||
#tags: Set<string> | null = null;
|
#tags: Set<string> | null = null;
|
||||||
|
|
||||||
@@ -49,6 +50,22 @@ export class RoomFactory {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the factory. Retrieves the room from the database and populates factory values.
|
||||||
|
*
|
||||||
|
* Does not fetch subroom content, only available subroom IDs.
|
||||||
|
*
|
||||||
|
* When using write mode, values are initialized to defaults.
|
||||||
|
*
|
||||||
|
* Defaults:
|
||||||
|
* - All hardware is supported
|
||||||
|
* - Cloning is not allowed
|
||||||
|
* - Room is not AG or dorm
|
||||||
|
* - State is Moderation_Closed
|
||||||
|
* - Accessibility is Unlisted
|
||||||
|
* - CreatorPlayerId is 1 (Coach)
|
||||||
|
* - Name and Description are empty strings
|
||||||
|
*/
|
||||||
async init(options: FactoryOptions) {
|
async init(options: FactoryOptions) {
|
||||||
if (typeof options.id == 'undefined' && typeof options.name == 'undefined')
|
if (typeof options.id == 'undefined' && typeof options.name == 'undefined')
|
||||||
throw new Error("Must specify a room ID or a room name");
|
throw new Error("Must specify a room ID or a room name");
|
||||||
@@ -63,11 +80,38 @@ export class RoomFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const obj = await this.#kv.getKv().get<DatabaseRoom>([RoomFactory.roomsKey, this.#roomId, 'meta']);
|
const obj = await this.#kv.getKv().get<DatabaseRoom>([RoomFactory.roomsKey, this.#roomId, 'meta']);
|
||||||
if (obj.value == null) return null;
|
if (options.mode == FactoryMode.Fetch && obj.value == null) return null;
|
||||||
else this.#obj = obj.value;
|
else this.#obj = options.mode == FactoryMode.Fetch ? obj.value : {
|
||||||
|
Hardware: new Set(HardwareSupports),
|
||||||
|
Tags: new Set(),
|
||||||
|
CoOwners: new Set(),
|
||||||
|
InvitedCoOwners: new Set(),
|
||||||
|
Hosts: new Set(),
|
||||||
|
InvitedHosts: new Set(),
|
||||||
|
Room: {
|
||||||
|
Name: "",
|
||||||
|
Description: "",
|
||||||
|
CreatorPlayerId: 1,
|
||||||
|
ImageName: "",
|
||||||
|
State: RoomState.Moderation_Closed,
|
||||||
|
Accessibility: RoomAccessibility.Unlisted,
|
||||||
|
SupportsLevelVoting: false,
|
||||||
|
IsAGRoom: false,
|
||||||
|
IsDormRoom: false,
|
||||||
|
CloningAllowed: false,
|
||||||
|
AllowsJuniors: true,
|
||||||
|
RoomWarningMask: 0,
|
||||||
|
CustomRoomWarning: "",
|
||||||
|
DisableMicAutoMute: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const subrooms = await this.#kv.getKv().get<Set<number>>([RoomFactory.roomsKey, this.#roomId, "subrooms"]);
|
||||||
|
if (options.mode == FactoryMode.Write || subrooms.value == null) this.#subrooms = new Set();
|
||||||
|
else this.#subrooms = subrooms.value;
|
||||||
|
|
||||||
const hardwareSupport = await this.#kv.getKv().get<Set<RoomDataTypes.HardwareSupport>>([RoomFactory.roomsKey, this.#roomId, 'hardware']);
|
const hardwareSupport = await this.#kv.getKv().get<Set<RoomDataTypes.HardwareSupport>>([RoomFactory.roomsKey, this.#roomId, 'hardware']);
|
||||||
if (hardwareSupport.value == null) return null;
|
if (hardwareSupport.value == null) this.#hardwareSupport = new Set(HardwareSupports);
|
||||||
else this.#hardwareSupport = hardwareSupport.value;
|
else this.#hardwareSupport = hardwareSupport.value;
|
||||||
|
|
||||||
const tags = await this.#kv.getKv().get<Set<string>>([RoomFactory.roomsKey, this.#roomId, 'tags']);
|
const tags = await this.#kv.getKv().get<Set<string>>([RoomFactory.roomsKey, this.#roomId, 'tags']);
|
||||||
@@ -102,7 +146,7 @@ export class RoomFactory {
|
|||||||
|
|
||||||
const key = [RoomFactory.roomsKey, this.#roomId, 'meta'];
|
const key = [RoomFactory.roomsKey, this.#roomId, 'meta'];
|
||||||
const val = await this.#kv.getKv().get(key);
|
const val = await this.#kv.getKv().get(key);
|
||||||
if (val == null) await w();
|
if (val.value == null) await w();
|
||||||
else {
|
else {
|
||||||
if (this.writeMode == WriteMode.Overwrite) await w();
|
if (this.writeMode == WriteMode.Overwrite) await w();
|
||||||
else throw new Error("Room already exists");
|
else throw new Error("Room already exists");
|
||||||
@@ -117,7 +161,7 @@ export class RoomFactory {
|
|||||||
const galvTags = this.getGalvanicTags();
|
const galvTags = this.getGalvanicTags();
|
||||||
|
|
||||||
const subroomExports = (await Promise.all(
|
const subroomExports = (await Promise.all(
|
||||||
this.getSubrooms().map(subroom => this.getSubroom(subroom))
|
this.getSubrooms().values().map(subroom => this.getSubroom(subroom))
|
||||||
)).map(factory => factory.export());
|
)).map(factory => factory.export());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -154,14 +198,36 @@ export class RoomFactory {
|
|||||||
VisitCount: await this.getVisitCount()
|
VisitCount: await this.getVisitCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setRoomProperties(props: RoomDataTypes.DatabaseRoomContent) {
|
||||||
|
Object.assign(this, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoomId() {
|
||||||
|
if (!this.#roomId) throw this.#cannotAccessBeforeInitError;
|
||||||
|
return this.#roomId;
|
||||||
|
}
|
||||||
|
|
||||||
getSubrooms() {
|
getSubrooms() {
|
||||||
if (!this.#obj) throw this.#cannotAccessBeforeInitError;
|
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
||||||
return this.#obj.Subrooms;
|
return this.#subrooms;
|
||||||
}
|
}
|
||||||
async getSubroom(id: number) {
|
async getSubroom(id: number) {
|
||||||
|
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
||||||
|
if (!this.#subrooms.has(id)) throw new Error("Subroom not available to this room");
|
||||||
return await new SubroomFactory(this.#server, this.#kv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
|
return await new SubroomFactory(this.#server, this.#kv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
|
||||||
}
|
}
|
||||||
|
getAvailableSubroomId() {
|
||||||
|
let id = Math.round(Math.random() * Math.pow(2, 31));
|
||||||
|
if (this.getSubrooms().has(id)) id = this.getAvailableSubroomId();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
async newSubroom(mode?: WriteMode) {
|
||||||
|
return await new SubroomFactory(this.#server, this.#kv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: this.getAvailableSubroomId() });
|
||||||
|
}
|
||||||
|
addSubroom(id: number) {
|
||||||
|
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
||||||
|
this.#subrooms.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
get Name() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.Name }
|
get Name() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.Name }
|
||||||
set Name(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.Name = data }
|
set Name(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.Name = data }
|
||||||
@@ -178,8 +244,8 @@ export class RoomFactory {
|
|||||||
get State() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.State }
|
get State() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.State }
|
||||||
set State(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.State = data }
|
set State(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.State = data }
|
||||||
|
|
||||||
get RoomAccessibility() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.Accessibility }
|
get Accessibility() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.Accessibility }
|
||||||
set RoomAccessibility(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.Accessibility = data }
|
set Accessibility(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.Accessibility = data }
|
||||||
|
|
||||||
get SupportsLevelVoting() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.SupportsLevelVoting }
|
get SupportsLevelVoting() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.SupportsLevelVoting }
|
||||||
set SupportsLevelVoting(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.SupportsLevelVoting = data }
|
set SupportsLevelVoting(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.SupportsLevelVoting = data }
|
||||||
@@ -229,6 +295,20 @@ export class RoomFactory {
|
|||||||
this.#hardwareSupport.delete(hardware);
|
this.#hardwareSupport.delete(hardware);
|
||||||
await this.#saveHardwareSupport();
|
await this.#saveHardwareSupport();
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Removes support for every hardware type in the room. When saving immediately after this, no client can legally join the room.
|
||||||
|
*
|
||||||
|
* In production, ensure at least one hardware type is supported.
|
||||||
|
*/
|
||||||
|
removeAllHardwareSupport() {
|
||||||
|
this.#hardwareSupport = new Set();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds support for every hardware type in the room.
|
||||||
|
*/
|
||||||
|
addAllHardwareSupport() {
|
||||||
|
this.#hardwareSupport = new Set(HardwareSupports);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visits are fetched during every access, not during init
|
* Visits are fetched during every access, not during init
|
||||||
@@ -252,8 +332,8 @@ export class RoomFactory {
|
|||||||
const tags: GalvanicTagDTO[] = [];
|
const tags: GalvanicTagDTO[] = [];
|
||||||
|
|
||||||
if (this.IsAGRoom) tags.push({ Tag: "recroomoriginal", Type: TagType.AGOnly });
|
if (this.IsAGRoom) tags.push({ Tag: "recroomoriginal", Type: TagType.AGOnly });
|
||||||
if (this.IsDormRoom) tags.push({ Tag: "dormroom", Type: TagType.Auto });
|
if (this.IsDormRoom) tags.push({ Tag: "dormroom", Type: TagType.AGOnly });
|
||||||
if (this.Name === "Paintball" || this.Name === "PaintballVR") tags.push({ Tag: "paintball", Type: TagType.Auto });
|
if (this.Name === "Paintball" || this.Name === "PaintballVR") tags.push({ Tag: "paintball", Type: TagType.AGOnly });
|
||||||
const hardwareSupport = this.getHardwareSupport();
|
const hardwareSupport = this.getHardwareSupport();
|
||||||
if (hardwareSupport.has('screens')) tags.push({ Tag: "screen", Type: TagType.Auto });
|
if (hardwareSupport.has('screens')) tags.push({ Tag: "screen", Type: TagType.Auto });
|
||||||
if (hardwareSupport.has('walk_vr')) tags.push({ Tag: "walkvr", Type: TagType.Auto });
|
if (hardwareSupport.has('walk_vr')) tags.push({ Tag: "walkvr", Type: TagType.Auto });
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
import type KV from "../../persistence/kv.ts";
|
import type KV from "../../persistence/kv.ts";
|
||||||
import { type ServerBase } from "../../server.ts";
|
import { type ServerBase } from "../../server.ts";
|
||||||
import { DatabaseSubroom, FactoryMode, RoomDataTypes, RoomSave, WriteMode } from "./RoomDataTypes.ts";
|
import { DatabaseSubroom, FactoryMode, RoomDataTypes, RoomSave, RoomSaveMap, WriteMode } from "./RoomDataTypes.ts";
|
||||||
|
|
||||||
export interface SubroomFactoryOptions {
|
export interface SubroomFactoryOptions {
|
||||||
mode: FactoryMode,
|
mode: FactoryMode,
|
||||||
@@ -8,6 +9,8 @@ export interface SubroomFactoryOptions {
|
|||||||
id: number
|
id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const log = new Logging("SubroomFactoryBase");
|
||||||
|
|
||||||
export class SubroomFactory {
|
export class SubroomFactory {
|
||||||
|
|
||||||
#server: ServerBase;
|
#server: ServerBase;
|
||||||
@@ -19,7 +22,7 @@ export class SubroomFactory {
|
|||||||
writeMode: WriteMode = WriteMode.WriteIfFree;
|
writeMode: WriteMode = WriteMode.WriteIfFree;
|
||||||
|
|
||||||
#obj: DatabaseSubroom | null = null;
|
#obj: DatabaseSubroom | null = null;
|
||||||
#saves: RoomSave[] | null = null;
|
#saves: RoomSaveMap | null = null;
|
||||||
|
|
||||||
#cannotAccessBeforeInitError = new Error("Cannot access properties before initialization");
|
#cannotAccessBeforeInitError = new Error("Cannot access properties before initialization");
|
||||||
#cannotWriteBeforeInitError = new Error("Cannot write before initialization");
|
#cannotWriteBeforeInitError = new Error("Cannot write before initialization");
|
||||||
@@ -32,13 +35,22 @@ export class SubroomFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init(options: SubroomFactoryOptions) {
|
async init(options: SubroomFactoryOptions) {
|
||||||
|
|
||||||
const data = await this.#kv.getKv().get<DatabaseSubroom>([options.id, 'meta']);
|
const data = await this.#kv.getKv().get<DatabaseSubroom>([options.id, 'meta']);
|
||||||
if (data == null && this.factoryMode == FactoryMode.Fetch) throw new Error("No such subroom");
|
if (options.mode == FactoryMode.Fetch && data == null) throw new Error("No such subroom");
|
||||||
|
|
||||||
const saves = await this.#kv.getKv().get<RoomSave[]>([options.id, 'saves']);
|
const saves = await this.#kv.getKv().get<RoomSaveMap>([options.id, 'saves']);
|
||||||
this.#saves = saves.value;
|
this.#saves = saves.value ?? new Map<number, RoomSave>();
|
||||||
|
|
||||||
this.#obj = data.value;
|
this.#obj = options.mode == FactoryMode.Fetch ? data.value : {
|
||||||
|
RoomId: 0,
|
||||||
|
RoomSceneLocationId: "",
|
||||||
|
Name: "Subroom data init failed, contact an admin!",
|
||||||
|
IsSandbox: false,
|
||||||
|
MaxPlayers: 8,
|
||||||
|
CanMatchmakeInto: true,
|
||||||
|
LatestSaveId: null
|
||||||
|
}; // use template object when writing
|
||||||
this.#subroomId = options.id;
|
this.#subroomId = options.id;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@@ -66,7 +78,7 @@ export class SubroomFactory {
|
|||||||
IsSandbox: this.IsSandbox,
|
IsSandbox: this.IsSandbox,
|
||||||
MaxPlayers: this.MaxPlayers,
|
MaxPlayers: this.MaxPlayers,
|
||||||
CanMatchmakeInto: this.CanMatchmakeInto,
|
CanMatchmakeInto: this.CanMatchmakeInto,
|
||||||
DataModifiedAt: save.SavedAt,
|
DataModifiedAt: save.SavedAt.toISOString(),
|
||||||
DataBlobName: save.DataBlobName
|
DataBlobName: save.DataBlobName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,24 +110,41 @@ export class SubroomFactory {
|
|||||||
if (!this.#saves) throw this.#cannotAccessBeforeInitError;
|
if (!this.#saves) throw this.#cannotAccessBeforeInitError;
|
||||||
|
|
||||||
let newId = Math.round(Math.random() * Math.pow(2, 31));
|
let newId = Math.round(Math.random() * Math.pow(2, 31));
|
||||||
while (this.#saves.some(save => save.SaveId == newId)) newId = this.#getAvailableSaveId();
|
while (this.#saves.has(newId)) newId = this.#getAvailableSaveId();
|
||||||
return newId;
|
return newId;
|
||||||
}
|
}
|
||||||
addSave(dataBlobName: string) {
|
/**
|
||||||
|
* Add a save (history) to the scene. Automatically resets the LatestSaveId.
|
||||||
|
*
|
||||||
|
* Use empty string for no datablob
|
||||||
|
*
|
||||||
|
* @param dataBlobName Filename of datablob on CDN
|
||||||
|
*/
|
||||||
|
addSave(dataBlobName?: string) {
|
||||||
if (!this.#saves) throw this.#cannotAccessBeforeInitError;
|
if (!this.#saves) throw this.#cannotAccessBeforeInitError;
|
||||||
this.#saves.push({
|
|
||||||
SaveId: this.#getAvailableSaveId(),
|
const newId = this.#getAvailableSaveId();
|
||||||
DataBlobName: dataBlobName,
|
this.#saves.set(newId, {
|
||||||
SavedAt: new Date().toISOString()
|
DataBlobName: dataBlobName ?? "",
|
||||||
|
SavedAt: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.LatestSaveId = newId;
|
||||||
}
|
}
|
||||||
getSaves() {
|
getSaves() {
|
||||||
if (!this.#saves) throw this.#cannotAccessBeforeInitError;
|
if (!this.#saves) throw this.#cannotAccessBeforeInitError;
|
||||||
return this.#saves;
|
return this.#saves;
|
||||||
}
|
}
|
||||||
getLatestSave() {
|
getLatestSave() {
|
||||||
if (!this.#saves) throw this.#cannotAccessBeforeInitError;
|
if (!this.#saves || !this.#obj) throw this.#cannotAccessBeforeInitError;
|
||||||
return this.#saves.find(save => save.SaveId == this.LatestSaveId);
|
else if (!this.#obj.LatestSaveId) throw new Error(`No save is marked as the latest save`);
|
||||||
|
else {
|
||||||
|
if (this.#saves.size === 0) {
|
||||||
|
log.w(`No save could be found when fetching the latest save for subroomid ${this.#subroomId}!`);
|
||||||
|
return null;
|
||||||
|
} else if (this.#saves.size === 1) return this.#saves.values().toArray()[0];
|
||||||
|
else return this.#saves.get(this.#obj.LatestSaveId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,15 @@ import z from "zod";
|
|||||||
import { verify } from "@hono/hono/jwt";
|
import { verify } from "@hono/hono/jwt";
|
||||||
import Server from "../server/server.ts";
|
import Server from "../server/server.ts";
|
||||||
import { TokenFormat } from "../server/platforms/types.ts";
|
import { TokenFormat } from "../server/platforms/types.ts";
|
||||||
|
import { HTTPStatus, httpStatusText } from "@oneday/http-status";
|
||||||
|
import { ContentfulStatusCode } from "@hono/hono/utils/http-status";
|
||||||
|
|
||||||
const log = new Logging("APIUtils");
|
const log = new Logging("APIUtils");
|
||||||
|
|
||||||
|
export function statusResponse(c: Context, code: HTTPStatus, msg?: string) {
|
||||||
|
return c.json(genericResponse(code < 400, msg ?? httpStatusText(code)), code as ContentfulStatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
export function genericResponse(success: boolean, msg?: string, data?: null) {
|
export function genericResponse(success: boolean, msg?: string, data?: null) {
|
||||||
return { success, msg, data }
|
return { success, msg, data }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ export async function importer<T>(importKey: string, prefix: string, paths: stri
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHonoRoute(path: string): RouteImport {
|
export function createHonoRoute(path: string) {
|
||||||
const route: RouteImport = {
|
const route: RouteImport = {
|
||||||
path,
|
path,
|
||||||
app: new Hono<HonoEnv>()
|
app: new Hono<HonoEnv>()
|
||||||
}
|
}
|
||||||
return route
|
return route;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user