rooooms
This commit is contained in:
@@ -13,9 +13,7 @@ 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 image = sharp(data);
|
||||
const rootMetadata = await image.metadata();
|
||||
const rootWidth = rootMetadata.width;
|
||||
const rootHeight = rootMetadata.height;
|
||||
const squareSize = Math.min(rootWidth, rootHeight);
|
||||
const squareSize = Math.min(rootMetadata.width, rootMetadata.height);
|
||||
if (query.cropSquare) image.resize(squareSize, squareSize);
|
||||
|
||||
const newImage = sharp(await image.png().toBuffer());
|
||||
@@ -34,8 +32,8 @@ const imgNameParamSchema = z.object({
|
||||
});
|
||||
const imgQuerySchema = z.object({
|
||||
cropSquare: z.coerce.boolean().optional(),
|
||||
width: z.coerce.number().max(3840).optional(),
|
||||
height: z.coerce.number().max(2160).optional(),
|
||||
width: z.coerce.number().min(64).max(3840).optional(),
|
||||
height: z.coerce.number().min(64).max(2160).optional(),
|
||||
});
|
||||
route.app.get('/:imgName',
|
||||
typedZValidator('param', imgNameParamSchema),
|
||||
|
||||
@@ -37,6 +37,7 @@ interface MessageBase {
|
||||
|
||||
export class ProfileMessageManager extends ProfileContentManager {
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
async getMessages() {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ interface ProfileReputation {
|
||||
|
||||
export class ProfileReputationManager extends ProfileContentManager {
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
async export(): Promise<ProfileReputation> {
|
||||
return {
|
||||
AccountId: this.profile.getId(),
|
||||
|
||||
@@ -25,7 +25,7 @@ export class ProfileSettingsManager extends ProfileContentManager {
|
||||
else return [];
|
||||
}
|
||||
|
||||
async getSetting(setting: ProfileSetting) {
|
||||
async getSetting(setting: string) {
|
||||
const settings = await this.getAllSettings();
|
||||
return settings.find(s => s.Key == setting)?.Value || null;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SelfAccount, type RecNetAccount } from "./types/profile.ts";
|
||||
import Command from "./../commands/command.ts";
|
||||
import z from "zod";
|
||||
import { PlatformMask, PlatformType, ProfileRole } from "../platforms/types.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
|
||||
const profiles: Map<number, Profile> = new Map();
|
||||
|
||||
@@ -13,6 +14,8 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
static profilesKey = "profiles";
|
||||
static profileByNameKey = "profileName";
|
||||
|
||||
#log = new Logging("ProfileManager");
|
||||
|
||||
async #getUnusedId() {
|
||||
let id = Math.round(Math.random() * 2_147_483_647);
|
||||
if (await this.get(id)) id = await this.#getUnusedId();
|
||||
@@ -136,6 +139,7 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
})
|
||||
]
|
||||
}));
|
||||
this.#log.i('Profile manager initialized');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import KV from "../persistence/kv.ts";
|
||||
import type Profile from "../profiles/profile.ts";
|
||||
import { RoomFactory } from "./internal/RoomFactory.ts";
|
||||
import { FactoryMode } from "./internal/RoomDataTypes.ts";
|
||||
|
||||
export class ServerRoomsBase extends ServerContentBase {
|
||||
|
||||
#roomsKey = "rooms";
|
||||
#roomNamesKey = "room_names"
|
||||
#subroomKv = new KV('subrooms', true);
|
||||
#log = new Logging("Rooms");
|
||||
|
||||
static roomNamesKey = "room_names";
|
||||
static playerDormsKey = "dorms";
|
||||
|
||||
protected override async start() {
|
||||
await this.#subroomKv.init();
|
||||
this.#log.i('[sub]rooms database initialized');
|
||||
}
|
||||
|
||||
getKv() {
|
||||
return this.kv;
|
||||
}
|
||||
|
||||
async getIdFromName(name: string) {
|
||||
const id = await this.kv.getKv().get<number>([this.#roomsKey, name]);
|
||||
const id = await this.kv.getKv().get<number>([ServerRoomsBase.roomNamesKey, name]);
|
||||
if (id.value == null) return null;
|
||||
return id.value;
|
||||
}
|
||||
|
||||
async getPlayerDorm(profile: Profile) {
|
||||
const id = await this.kv.getKv().get<number>([ServerRoomsBase.playerDormsKey, profile.getId()]);
|
||||
if (id.value == null) return null;
|
||||
else return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Fetch, id: id.value });
|
||||
}
|
||||
|
||||
async getFromName(name: string) {
|
||||
|
||||
const id = await this.getIdFromName(name);
|
||||
if (id == null) return null;
|
||||
return await this.get(id);
|
||||
}
|
||||
|
||||
async get(id: number) {
|
||||
|
||||
return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Fetch, id: id });
|
||||
}
|
||||
|
||||
}
|
||||
58
src/server/rooms/internal/ClientRoomTypes.ts
Normal file
58
src/server/rooms/internal/ClientRoomTypes.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { RoomAccessibility } from "./RoomDataTypes.ts";
|
||||
|
||||
export interface AGRoomScene {
|
||||
Name: string,
|
||||
RoomSceneLocationId: string,
|
||||
IsSandbox: boolean,
|
||||
CanMatchmakeInto: boolean,
|
||||
SupportsJoinInProgress: boolean,
|
||||
UseLevelBasedMatchmaking: boolean,
|
||||
UseAgeBasedMatchmaking: boolean,
|
||||
UseRecRoyaleMatchmaking: boolean,
|
||||
MaxPlayers: number
|
||||
}
|
||||
|
||||
export interface AGRoom {
|
||||
Name: string,
|
||||
Description: string,
|
||||
Accessibility: RoomAccessibility,
|
||||
SupportsLevelVoting: boolean,
|
||||
CloningAllowed: boolean,
|
||||
SupportsScreens: boolean,
|
||||
SupportsWalkVR: boolean,
|
||||
SupportsTeleportVR: boolean,
|
||||
Scenes: AGRoomScene[]
|
||||
}
|
||||
|
||||
export enum AGRoomReleaseStatus {
|
||||
EditorOnly,
|
||||
TestServersOnly,
|
||||
Released
|
||||
}
|
||||
|
||||
export interface AGRoomLocation {
|
||||
Name: string,
|
||||
ReplicationId: string,
|
||||
SceneName: string,
|
||||
RequiredSubSceneNames: string[],
|
||||
LevelRoomSubSceneNames: string[],
|
||||
SupportsVRLow: boolean,
|
||||
SupportsMobile: boolean,
|
||||
MaxPlayers: number,
|
||||
ReleaseStatus: AGRoomReleaseStatus,
|
||||
EmptyOnSandboxClone: boolean,
|
||||
SupportedGameMode: number,
|
||||
GameTeamColorSettings: {
|
||||
TeamOutfitColorEmissionEnabled: boolean,
|
||||
TeamOutfitColorEmissionAmount: number,
|
||||
CustomTeamColors: {
|
||||
Team: number,
|
||||
Color: {
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
a: number
|
||||
}
|
||||
}[]
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
import { RoomLocation } from "../../instances/base.ts";
|
||||
import { RoomFactory } from "./RoomFactory.ts";
|
||||
import { SubroomFactory } from "./SubroomFactory.ts";
|
||||
|
||||
export enum WriteMode {
|
||||
Overwrite = "overwrite",
|
||||
WriteIfFree = "if_free"
|
||||
@@ -12,8 +8,11 @@ export enum FactoryMode {
|
||||
Write = 'write'
|
||||
}
|
||||
|
||||
export type HardwareSupport = "screens" | "walk_vr" | "teleport_vr" | "low_vr" | "mobile";
|
||||
export const HardwareSupportStrings = ["screens", "walk_vr", "teleport_vr", "low_vr", "mobile"];
|
||||
export const HardwareSupports = ["screens", "walk_vr", "teleport_vr", "low_vr", "mobile"] as const;
|
||||
export type HardwareSupport = typeof HardwareSupports[number];
|
||||
|
||||
export const GalvanicRoomTags = ["recroomoriginal", "dormroom", "paintball", "screen", "walkvr", "teleportvr", "junior"] as const;
|
||||
export type GalvanicRoomTag = typeof GalvanicRoomTags[number];
|
||||
|
||||
export enum RoomState {
|
||||
Active,
|
||||
@@ -29,30 +28,6 @@ export enum RoomAccessibility {
|
||||
Unlisted
|
||||
}
|
||||
|
||||
export interface BuiltinScene {
|
||||
Name: string,
|
||||
RoomSceneLocationId: RoomLocation,
|
||||
IsSandbox: boolean,
|
||||
CanMatchmakeInto: boolean,
|
||||
SupportsJoinInProgress: boolean,
|
||||
UseLevelBasedMatchmaking: boolean,
|
||||
UseAgeBasedMatchmaking: boolean,
|
||||
UseRecRoyaleMatchmaking: boolean,
|
||||
MaxPlayers: number
|
||||
}
|
||||
|
||||
export interface BuiltinRoom {
|
||||
Name: string,
|
||||
Description: string,
|
||||
Accessibility: RoomAccessibility,
|
||||
SupportsLevelVoting: boolean,
|
||||
CloningAllowed: boolean,
|
||||
SupportsScreens: boolean,
|
||||
SupportsWalkVR: boolean,
|
||||
SupportsTeleportVR: boolean,
|
||||
Scenes: BuiltinScene[]
|
||||
}
|
||||
|
||||
export interface RoomScene {
|
||||
RoomSceneId: number,
|
||||
RoomId: number,
|
||||
@@ -65,24 +40,15 @@ export interface RoomScene {
|
||||
DataModifiedAt: string
|
||||
}
|
||||
|
||||
export interface Room {
|
||||
export interface Room extends DatabaseRoomContent {
|
||||
RoomId: number,
|
||||
Name: string,
|
||||
Description: string,
|
||||
CreatorPlayerId: number,
|
||||
ImageName: string,
|
||||
State: RoomState,
|
||||
Accessibility: RoomAccessibility,
|
||||
SupportsLevelVoting: boolean,
|
||||
IsAGRoom: boolean,
|
||||
IsDormRoom?: boolean,
|
||||
CloningAllowed: boolean,
|
||||
SupportsScreens: boolean,
|
||||
SupportsWalkVR: boolean,
|
||||
SupportsTeleportVR: boolean,
|
||||
SupportsMobile: boolean,
|
||||
SupportsVRLow: boolean,
|
||||
AllowsJuniors: boolean,
|
||||
RoomWarningMask: number,
|
||||
CustomRoomWarning: string,
|
||||
DisableMicAutoMute?: boolean | null
|
||||
}
|
||||
|
||||
@@ -103,6 +69,10 @@ export enum TagType {
|
||||
Banned
|
||||
}
|
||||
|
||||
export interface GalvanicTagDTO {
|
||||
Tag: GalvanicRoomTag,
|
||||
Type: TagType
|
||||
}
|
||||
export interface TagDTO {
|
||||
Tag: string,
|
||||
Type: TagType
|
||||
@@ -120,7 +90,7 @@ export interface RoomDetails {
|
||||
CheerCount: number,
|
||||
FavoriteCount: number,
|
||||
VisitCount: number,
|
||||
Tags: TagDTO[]
|
||||
Tags: (GalvanicTagDTO | TagDTO)[]
|
||||
}
|
||||
|
||||
export enum CreateModifyRoomStatus {
|
||||
@@ -150,29 +120,48 @@ export enum CreateModifyRoomStatus {
|
||||
PlayerIsRoomBanned = 410
|
||||
}
|
||||
|
||||
export interface DatabaseRoomContent {
|
||||
Name: string,
|
||||
Description: string,
|
||||
CreatorPlayerId: number,
|
||||
ImageName: string,
|
||||
State: RoomState,
|
||||
Accessibility: RoomAccessibility,
|
||||
SupportsLevelVoting: boolean,
|
||||
IsAGRoom: boolean,
|
||||
IsDormRoom: boolean,
|
||||
CloningAllowed: boolean,
|
||||
AllowsJuniors: boolean,
|
||||
RoomWarningMask: number,
|
||||
CustomRoomWarning: string,
|
||||
DisableMicAutoMute?: boolean | null
|
||||
}
|
||||
export interface DatabaseRoom {
|
||||
Visits: number,
|
||||
Hardware: Set<HardwareSupport>,
|
||||
Subrooms: number[],
|
||||
Tags: Set<string>,
|
||||
Tags: Set<number>,
|
||||
CoOwners: Set<number>,
|
||||
InvitedCoOwners: Set<number>,
|
||||
Hosts: Set<number>,
|
||||
InvitedHosts: Set<number>,
|
||||
Favorites: Set<number>,
|
||||
Cheers: Set<number>
|
||||
Room: DatabaseRoomContent
|
||||
}
|
||||
|
||||
export interface DatabaseSubroom {
|
||||
|
||||
RoomId: number,
|
||||
RoomSceneLocationId: string,
|
||||
Name: string,
|
||||
IsSandbox: boolean,
|
||||
MaxPlayers: number,
|
||||
CanMatchmakeInto: boolean,
|
||||
LatestSaveId: number,
|
||||
Saves: RoomSave[]
|
||||
}
|
||||
|
||||
export interface RoomUpdatedEvent {
|
||||
room: RoomFactory
|
||||
export interface RoomSave {
|
||||
SaveId: number,
|
||||
DataBlobName: string,
|
||||
SavedAt: string
|
||||
}
|
||||
|
||||
export interface SubroomUpdatedEvent {
|
||||
subroom: SubroomFactory
|
||||
}
|
||||
|
||||
export * as RoomDataTypes from "./DataTypes.ts";
|
||||
export * as RoomDataTypes from "./RoomDataTypes.ts";
|
||||
10
src/server/rooms/internal/RoomEvents.ts
Normal file
10
src/server/rooms/internal/RoomEvents.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { RoomFactory } from "./RoomFactory.ts";
|
||||
import { SubroomFactory } from "./SubroomFactory.ts";
|
||||
|
||||
export interface RoomUpdatedEvent {
|
||||
room: RoomFactory
|
||||
}
|
||||
|
||||
export interface SubroomUpdatedEvent {
|
||||
subroom: SubroomFactory
|
||||
}
|
||||
@@ -1,53 +1,273 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import type KV from "../../persistence/kv.ts";
|
||||
import { type ServerBase } from "../../server.ts";
|
||||
import { DatabaseRoom, RoomDataTypes } from "./DataTypes.ts";
|
||||
import { DatabaseRoom, FactoryMode, GalvanicTagDTO, RoomDataTypes, TagDTO, TagType, WriteMode } from "./RoomDataTypes.ts";
|
||||
import { SubroomFactory } from "./SubroomFactory.ts";
|
||||
import { ServerRoomsBase } from "../base.ts";
|
||||
|
||||
export interface RoomFactoryOptions {
|
||||
const log = new Logging("RoomFactory");
|
||||
|
||||
const roomDebug = true;
|
||||
|
||||
interface FactoryOptionsBase {
|
||||
mode: FactoryMode,
|
||||
id?: number,
|
||||
name?: string,
|
||||
name?: string
|
||||
}
|
||||
export interface WriteFactoryOptions extends FactoryOptionsBase {
|
||||
mode: FactoryMode.Write,
|
||||
writeMode: WriteMode,
|
||||
}
|
||||
export interface FetchFactoryOptions extends FactoryOptionsBase {
|
||||
mode: FactoryMode.Fetch
|
||||
}
|
||||
export type FactoryOptions = WriteFactoryOptions | FetchFactoryOptions;
|
||||
|
||||
export class RoomFactory {
|
||||
|
||||
static roomsKey = "rooms";
|
||||
|
||||
#server: ServerBase;
|
||||
#kv: KV;
|
||||
|
||||
#roomId: number | undefined;
|
||||
#name: string | undefined;
|
||||
#description: string | undefined;
|
||||
#creatorPlayerId: number | undefined;
|
||||
#imageName: string | undefined;
|
||||
#state: RoomDataTypes.RoomScene | undefined;
|
||||
#accessibility: RoomDataTypes.RoomAccessibility | undefined;
|
||||
#supportsLevelVoting: boolean | undefined;
|
||||
#isAGRoom: boolean | undefined;
|
||||
#isDormRoom: boolean | undefined;
|
||||
#cloningAllowed: boolean | undefined;
|
||||
#supportsScreens: boolean | undefined;
|
||||
#supportsWalkVR: boolean | undefined;
|
||||
#supportsTeleportVR: boolean | undefined;
|
||||
#allowsJuniors: boolean | undefined;
|
||||
#roomWarningMask: number | undefined;
|
||||
#customRoomWarning: string | undefined;
|
||||
#disableMicAutoMute: boolean | undefined;
|
||||
|
||||
#dbMeta: DatabaseRoom | null = null;
|
||||
factoryMode: FactoryMode = FactoryMode.Fetch;
|
||||
writeMode: WriteMode = WriteMode.WriteIfFree;
|
||||
|
||||
#visits: number | undefined;
|
||||
#hardware
|
||||
#obj: DatabaseRoom | null = null;
|
||||
#hardwareSupport: Set<RoomDataTypes.HardwareSupport> | null = null;
|
||||
#tags: Set<string> | null = null;
|
||||
|
||||
#cannotAccessBeforeInitError = new Error("Cannot access properties before initialization");
|
||||
#cannotWriteBeforeInitError = new Error("Cannot write before initialization");
|
||||
|
||||
constructor(server: ServerBase, kv: KV) {
|
||||
|
||||
|
||||
this.#server = server;
|
||||
this.#kv = kv;
|
||||
|
||||
}
|
||||
|
||||
async init(options: RoomFactoryOptions) {
|
||||
async init(options: FactoryOptions) {
|
||||
if (typeof options.id == 'undefined' && typeof options.name == 'undefined')
|
||||
throw new Error("Must specify a room ID or a room name");
|
||||
|
||||
if (typeof options.id !== 'undefined') this.#roomId = options.id;
|
||||
else {
|
||||
if (!options.name) throw new Error("Room name must be provided");
|
||||
|
||||
const id = await this.#server.Rooms.getIdFromName(options.name);
|
||||
if (id == null) throw new Error("Room name not found");
|
||||
this.#roomId = id;
|
||||
}
|
||||
|
||||
const obj = await this.#kv.getKv().get<DatabaseRoom>([RoomFactory.roomsKey, this.#roomId, 'meta']);
|
||||
if (obj.value == null) return null;
|
||||
else this.#obj = obj.value;
|
||||
|
||||
const hardwareSupport = await this.#kv.getKv().get<Set<RoomDataTypes.HardwareSupport>>([RoomFactory.roomsKey, this.#roomId, 'hardware']);
|
||||
if (hardwareSupport.value == null) return null;
|
||||
else this.#hardwareSupport = hardwareSupport.value;
|
||||
|
||||
const tags = await this.#kv.getKv().get<Set<string>>([RoomFactory.roomsKey, this.#roomId, 'tags']);
|
||||
this.#tags = tags.value;
|
||||
|
||||
this.factoryMode = options.mode;
|
||||
if (options.mode == FactoryMode.Write) this.writeMode = options.writeMode;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async write() {
|
||||
if (typeof this.#roomId !== 'number') throw this.#cannotWriteBeforeInitError;
|
||||
|
||||
const w = async () => {
|
||||
if (typeof this.#roomId !== 'number') throw this.#cannotWriteBeforeInitError;
|
||||
|
||||
await Promise.all([
|
||||
this.#kv.getKv().set([RoomFactory.roomsKey, this.#roomId, 'meta'], this.#obj),
|
||||
this.#kv.getKv().set([RoomFactory.roomsKey, this.#roomId, 'hardware'], this.#hardwareSupport),
|
||||
this.#kv.getKv().set([RoomFactory.roomsKey, this.#roomId, 'tags'], this.#tags),
|
||||
]);
|
||||
|
||||
if (!this.IsDormRoom) this.#kv.getKv().set([ServerRoomsBase.roomNamesKey, this.Name], this.#roomId);
|
||||
else this.#kv.getKv().set([ServerRoomsBase.playerDormsKey, this.CreatorPlayerId], this.#roomId);
|
||||
|
||||
this.#server.emit('room.updated', { room: this });
|
||||
if (roomDebug) log.d(`Room ${this.#roomId} written and emitted`);
|
||||
}
|
||||
|
||||
if (this.factoryMode == FactoryMode.Fetch) throw new Error("Cannot write in fetch mode");
|
||||
|
||||
const key = [RoomFactory.roomsKey, this.#roomId, 'meta'];
|
||||
const val = await this.#kv.getKv().get(key);
|
||||
if (val == null) await w();
|
||||
else {
|
||||
if (this.writeMode == WriteMode.Overwrite) await w();
|
||||
else throw new Error("Room already exists");
|
||||
}
|
||||
}
|
||||
|
||||
async export(): Promise<RoomDataTypes.RoomDetails> {
|
||||
if (!this.#obj || !this.#roomId) throw this.#cannotAccessBeforeInitError;
|
||||
const obj = this.#obj;
|
||||
const hardwareSupport = this.getHardwareSupport();
|
||||
const autoTags = this.getTags();
|
||||
const galvTags = this.getGalvanicTags();
|
||||
|
||||
const subroomExports = (await Promise.all(
|
||||
this.getSubrooms().map(subroom => this.getSubroom(subroom))
|
||||
)).map(factory => factory.export());
|
||||
|
||||
return {
|
||||
Room: {
|
||||
RoomId: this.#roomId,
|
||||
Name: obj.Room.Name,
|
||||
Description: obj.Room.Description,
|
||||
CreatorPlayerId: obj.Room.CreatorPlayerId,
|
||||
ImageName: obj.Room.ImageName,
|
||||
State: obj.Room.State,
|
||||
Accessibility: obj.Room.Accessibility,
|
||||
SupportsLevelVoting: obj.Room.SupportsLevelVoting,
|
||||
IsAGRoom: obj.Room.IsAGRoom,
|
||||
IsDormRoom: obj.Room.IsDormRoom,
|
||||
CloningAllowed: obj.Room.CloningAllowed,
|
||||
AllowsJuniors: obj.Room.AllowsJuniors,
|
||||
RoomWarningMask: obj.Room.RoomWarningMask,
|
||||
CustomRoomWarning: obj.Room.CustomRoomWarning,
|
||||
DisableMicAutoMute: obj.Room.DisableMicAutoMute,
|
||||
SupportsScreens: hardwareSupport.has('screens'),
|
||||
SupportsWalkVR: hardwareSupport.has('walk_vr'),
|
||||
SupportsTeleportVR: hardwareSupport.has('teleport_vr'),
|
||||
SupportsMobile: hardwareSupport.has('mobile'),
|
||||
SupportsVRLow: hardwareSupport.has('low_vr')
|
||||
},
|
||||
Scenes: subroomExports,
|
||||
Tags: autoTags.concat(galvTags),
|
||||
CoOwners: [],
|
||||
InvitedCoOwners: [],
|
||||
Hosts: [],
|
||||
InvitedHosts: [],
|
||||
CheerCount: 0,
|
||||
FavoriteCount: 0,
|
||||
VisitCount: await this.getVisitCount()
|
||||
}
|
||||
}
|
||||
|
||||
getSubrooms() {
|
||||
if (!this.#obj) throw this.#cannotAccessBeforeInitError;
|
||||
return this.#obj.Subrooms;
|
||||
}
|
||||
async getSubroom(id: number) {
|
||||
return await new SubroomFactory(this.#server, this.#kv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
get Description() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.Description }
|
||||
set Description(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.Description = data }
|
||||
|
||||
get CreatorPlayerId() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.CreatorPlayerId }
|
||||
set CreatorPlayerId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.CreatorPlayerId = data }
|
||||
|
||||
get ImageName() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.ImageName }
|
||||
set ImageName(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.ImageName = data }
|
||||
|
||||
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 }
|
||||
|
||||
get RoomAccessibility() { 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 }
|
||||
|
||||
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 }
|
||||
|
||||
get IsAGRoom() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.IsAGRoom }
|
||||
set IsAGRoom(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.IsAGRoom = data }
|
||||
|
||||
get IsDormRoom() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.IsDormRoom }
|
||||
set IsDormRoom(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.IsDormRoom = data }
|
||||
|
||||
get CloningAllowed() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.CloningAllowed }
|
||||
set CloningAllowed(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.CloningAllowed = data }
|
||||
|
||||
get AllowsJuniors() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.AllowsJuniors }
|
||||
set AllowsJuniors(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.AllowsJuniors = data }
|
||||
|
||||
get RoomWarningMask() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.RoomWarningMask }
|
||||
set RoomWarningMask(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.RoomWarningMask = data }
|
||||
|
||||
get CustomRoomWarning() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.CustomRoomWarning }
|
||||
set CustomRoomWarning(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.CustomRoomWarning = data }
|
||||
|
||||
get DisableMicAutoMute() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Room.DisableMicAutoMute }
|
||||
set DisableMicAutoMute(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; this.#obj.Room.DisableMicAutoMute = data }
|
||||
|
||||
getHardwareSupport() {
|
||||
if (!this.#hardwareSupport) throw this.#cannotAccessBeforeInitError;
|
||||
return this.#hardwareSupport;
|
||||
}
|
||||
async #saveHardwareSupport() {
|
||||
if (!this.#roomId) throw this.#cannotAccessBeforeInitError;
|
||||
await this.#kv.getKv().set([this.#roomId, 'hardware'], this.#hardwareSupport);
|
||||
}
|
||||
async addHardwareSupport(...hardware: RoomDataTypes.HardwareSupport[]) {
|
||||
if (!this.#hardwareSupport || !this.#roomId) throw this.#cannotAccessBeforeInitError;
|
||||
|
||||
if (typeof hardware == 'string') {
|
||||
this.#hardwareSupport.add(hardware);
|
||||
await this.#saveHardwareSupport();
|
||||
} else {
|
||||
for (const hw of hardware) this.#hardwareSupport.add(hw);
|
||||
await this.#saveHardwareSupport();
|
||||
}
|
||||
}
|
||||
async removeHardwareSupport(hardware: RoomDataTypes.HardwareSupport) {
|
||||
if (!this.#hardwareSupport) throw this.#cannotAccessBeforeInitError;
|
||||
this.#hardwareSupport.delete(hardware);
|
||||
await this.#saveHardwareSupport();
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits are fetched during every access, not during init
|
||||
*/
|
||||
async getVisitCount() {
|
||||
if (!this.#roomId) throw this.#cannotAccessBeforeInitError;
|
||||
const val = await this.#kv.getKv().get<number>([RoomFactory.roomsKey, this.#roomId, 'visits']);
|
||||
if (val.value == null) return 0;
|
||||
else return val.value;
|
||||
}
|
||||
/**
|
||||
* Visits are fetched during every access, not during init
|
||||
*/
|
||||
async addVisit() {
|
||||
if (!this.#roomId) throw this.#cannotAccessBeforeInitError;
|
||||
const visits = await this.getVisitCount();
|
||||
await this.#kv.getKv().set([RoomFactory.roomsKey, this.#roomId, 'visits'], visits + 1);
|
||||
}
|
||||
|
||||
getGalvanicTags(): GalvanicTagDTO[] {
|
||||
const tags: GalvanicTagDTO[] = [];
|
||||
|
||||
if (this.IsAGRoom) tags.push({ Tag: "recroomoriginal", Type: TagType.AGOnly });
|
||||
if (this.IsDormRoom) tags.push({ Tag: "dormroom", Type: TagType.Auto });
|
||||
if (this.Name === "Paintball" || this.Name === "PaintballVR") tags.push({ Tag: "paintball", Type: TagType.Auto });
|
||||
const hardwareSupport = this.getHardwareSupport();
|
||||
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('teleport_vr')) tags.push({ Tag: "teleportvr", Type: TagType.Auto });
|
||||
if (this.AllowsJuniors) tags.push({ Tag: "junior", Type: TagType.Auto });
|
||||
|
||||
return tags;
|
||||
}
|
||||
getTags(): TagDTO[] {
|
||||
if (!this.#tags) return [];
|
||||
return this.#tags.values().toArray().map(val => ({ Tag: val, Type: TagType.General }));
|
||||
}
|
||||
setTags(tags: Set<string>) {
|
||||
this.#tags = tags;
|
||||
}
|
||||
|
||||
}
|
||||
121
src/server/rooms/internal/SubroomFactory.ts
Normal file
121
src/server/rooms/internal/SubroomFactory.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type KV from "../../persistence/kv.ts";
|
||||
import { type ServerBase } from "../../server.ts";
|
||||
import { DatabaseSubroom, FactoryMode, RoomDataTypes, RoomSave, WriteMode } from "./RoomDataTypes.ts";
|
||||
|
||||
export interface SubroomFactoryOptions {
|
||||
mode: FactoryMode,
|
||||
writeMode: WriteMode
|
||||
id: number
|
||||
}
|
||||
|
||||
export class SubroomFactory {
|
||||
|
||||
#server: ServerBase;
|
||||
#kv: KV;
|
||||
|
||||
#subroomId: number | null = null;
|
||||
|
||||
factoryMode: FactoryMode = FactoryMode.Fetch;
|
||||
writeMode: WriteMode = WriteMode.WriteIfFree;
|
||||
|
||||
#obj: DatabaseSubroom | null = null;
|
||||
#saves: RoomSave[] | null = null;
|
||||
|
||||
#cannotAccessBeforeInitError = new Error("Cannot access properties before initialization");
|
||||
#cannotWriteBeforeInitError = new Error("Cannot write before initialization");
|
||||
|
||||
constructor(server: ServerBase, kv: KV) {
|
||||
|
||||
this.#server = server;
|
||||
this.#kv = kv;
|
||||
|
||||
}
|
||||
|
||||
async init(options: SubroomFactoryOptions) {
|
||||
const data = await this.#kv.getKv().get<DatabaseSubroom>([options.id, 'meta']);
|
||||
if (data == null && this.factoryMode == FactoryMode.Fetch) throw new Error("No such subroom");
|
||||
|
||||
const saves = await this.#kv.getKv().get<RoomSave[]>([options.id, 'saves']);
|
||||
this.#saves = saves.value;
|
||||
|
||||
this.#obj = data.value;
|
||||
this.#subroomId = options.id;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async write() {
|
||||
if (!this.#obj) throw this.#cannotWriteBeforeInitError;
|
||||
if (!this.#subroomId) throw new Error("No RoomSceneId set");
|
||||
|
||||
await this.#kv.getKv().set([this.#subroomId, 'meta'], this.#obj);
|
||||
await this.#kv.getKv().set([this.#subroomId, 'saves'], this.#saves);
|
||||
this.#server.emit('room.subroom.updated', { subroom: this });
|
||||
}
|
||||
|
||||
export(): RoomDataTypes.RoomScene {
|
||||
if (!this.#subroomId) throw this.#cannotAccessBeforeInitError;
|
||||
const save = this.getLatestSave();
|
||||
if (!save) throw new Error("No save to export");
|
||||
|
||||
return {
|
||||
RoomId: this.RoomId,
|
||||
RoomSceneId: this.#subroomId,
|
||||
RoomSceneLocationId: this.RoomSceneLocationId,
|
||||
Name: this.Name,
|
||||
IsSandbox: this.IsSandbox,
|
||||
MaxPlayers: this.MaxPlayers,
|
||||
CanMatchmakeInto: this.CanMatchmakeInto,
|
||||
DataModifiedAt: save.SavedAt,
|
||||
DataBlobName: save.DataBlobName
|
||||
}
|
||||
}
|
||||
|
||||
get RoomId() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.RoomId; }
|
||||
set RoomId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.RoomId = data }
|
||||
|
||||
get RoomSceneId() { if (!this.#subroomId) throw this.#cannotAccessBeforeInitError; else return this.#subroomId; }
|
||||
|
||||
get RoomSceneLocationId() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.RoomSceneLocationId; }
|
||||
set RoomSceneLocationId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.RoomSceneLocationId = data }
|
||||
|
||||
get Name() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.Name; }
|
||||
set Name(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.Name = data }
|
||||
|
||||
get IsSandbox() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.IsSandbox; }
|
||||
set IsSandbox(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.IsSandbox = data }
|
||||
|
||||
get MaxPlayers() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.MaxPlayers; }
|
||||
set MaxPlayers(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.MaxPlayers = data }
|
||||
|
||||
get CanMatchmakeInto() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.CanMatchmakeInto; }
|
||||
set CanMatchmakeInto(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.CanMatchmakeInto = data }
|
||||
|
||||
get LatestSaveId() { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else return this.#obj.LatestSaveId; }
|
||||
set LatestSaveId(data) { if (!this.#obj) throw this.#cannotAccessBeforeInitError; else this.#obj.LatestSaveId = data }
|
||||
|
||||
#getAvailableSaveId() {
|
||||
if (!this.#saves) throw this.#cannotAccessBeforeInitError;
|
||||
|
||||
let newId = Math.round(Math.random() * Math.pow(2, 31));
|
||||
while (this.#saves.some(save => save.SaveId == newId)) newId = this.#getAvailableSaveId();
|
||||
return newId;
|
||||
}
|
||||
addSave(dataBlobName: string) {
|
||||
if (!this.#saves) throw this.#cannotAccessBeforeInitError;
|
||||
this.#saves.push({
|
||||
SaveId: this.#getAvailableSaveId(),
|
||||
DataBlobName: dataBlobName,
|
||||
SavedAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
getSaves() {
|
||||
if (!this.#saves) throw this.#cannotAccessBeforeInitError;
|
||||
return this.#saves;
|
||||
}
|
||||
getLatestSave() {
|
||||
if (!this.#saves) throw this.#cannotAccessBeforeInitError;
|
||||
return this.#saves.find(save => save.SaveId == this.LatestSaveId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { type ProfileUpdateEvent } from "./profiles/events/ProfileUpdate.ts";
|
||||
import { type RoleUpdateEvent } from "./profiles/events/RoleUpdate.ts";
|
||||
import ProfileManagerBase from "./profiles/manager.ts";
|
||||
import { ServerRoomsBase } from "./rooms/base.ts";
|
||||
import { RoomUpdatedEvent, SubroomUpdatedEvent } from "./rooms/internal/DataTypes.ts";
|
||||
import { RoomUpdatedEvent, SubroomUpdatedEvent } from "./rooms/internal/RoomEvents.ts";
|
||||
|
||||
interface ServerEvents {
|
||||
'profile.roleupdate': RoleUpdateEvent,
|
||||
@@ -32,6 +32,10 @@ class ServerBase extends EventManager<ServerEvents> {
|
||||
Instances = new InstanceManager(this, 'instances');
|
||||
Content = new ServerContentManager(this, "content");
|
||||
Rooms = new ServerRoomsBase(this, 'rooms', true);
|
||||
|
||||
generateMask(...num: number[]) {
|
||||
return num.reduce((sum, val) => sum + val, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const Server = new ServerBase();
|
||||
|
||||
@@ -50,14 +50,18 @@ export default class SocketConsoleHandler {
|
||||
async #onMsg(ev: MessageEvent) {
|
||||
try {
|
||||
const parsed = JSON.parse(ev.data) as ConsoleItem;
|
||||
const zodd = ConsoleItemSchema.safeParse(parsed);
|
||||
if (!zodd.success) this.destroy();
|
||||
else if (zodd.data.e == ConsoleEvent.Command) {
|
||||
const data = await Server.Commands.dispatch(...zodd.data.d.split(' '));
|
||||
const zodParsed = ConsoleItemSchema.safeParse(parsed);
|
||||
|
||||
if (!zodParsed.success) this.destroy();
|
||||
|
||||
else if (zodParsed.data.e == ConsoleEvent.Command) {
|
||||
const data = await Server.Commands.dispatch(...zodParsed.data.d.split(' '));
|
||||
if (data instanceof Error) throw data;
|
||||
|
||||
this.send(ConsoleEvent.Message, chalk.gray(`> ${chalk.yellow(data)}`));
|
||||
}
|
||||
else if (zodd.data.e == ConsoleEvent.Close) this.destroy();
|
||||
else if (zodParsed.data.e == ConsoleEvent.Close) this.destroy();
|
||||
|
||||
} catch (err) {
|
||||
this.#log.e(err);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CompletionMessage,
|
||||
Message,
|
||||
MessageKind,
|
||||
NotificationHandler,
|
||||
PushNotificationId,
|
||||
SignalMessageType,
|
||||
SignalRMessage,
|
||||
@@ -164,7 +165,7 @@ export class SignalRSocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
sendNotification(id: PushNotificationId | string, args?: object) {
|
||||
sendNotification(id: PushNotificationId | NotificationHandler, args?: object) {
|
||||
const msg: SignalRMessage = {
|
||||
type: SignalMessageType.Invocation,
|
||||
target: "Notification",
|
||||
|
||||
@@ -182,4 +182,20 @@ export enum PushNotificationId {
|
||||
CommunityBoardUpdate = 95,
|
||||
CommunityBoardAnnouncementUpdate,
|
||||
InventionModerationStateChanged = 100,
|
||||
}
|
||||
}
|
||||
|
||||
const notificationHandlers = [
|
||||
"AccountUpdate",
|
||||
"SelfAccountUpdate",
|
||||
"AnnouncementUpdate",
|
||||
"AnnouncementDelete",
|
||||
"ChatMessageReceived",
|
||||
"CommerceSubscriptionUpdate",
|
||||
"RoomInstanceUpdate",
|
||||
"PresenceUpdate",
|
||||
"ModerationQuitGame",
|
||||
"AppVersionUpdate",
|
||||
"PlayerProgressionLevelUpdate",
|
||||
"ReputationUpdate"
|
||||
] as const;
|
||||
export type NotificationHandler = typeof notificationHandlers[number];
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Context, Next } from "@hono/hono";
|
||||
import { HonoEnv } from "./types.ts";
|
||||
import { type HonoEnv } from "./types.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import z from "zod";
|
||||
import { verify } from "@hono/hono/jwt";
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import type { MiddlewareHandler } from "@hono/hono";
|
||||
import { z, ZodObject } from "zod";
|
||||
import { z } from "zod";
|
||||
import type { HonoEnv } from "./types.ts";
|
||||
import { ZodSchema } from "zod/v4";
|
||||
|
||||
// thanks claude, this hurt my brain!
|
||||
|
||||
export const typedZValidator = <
|
||||
Target extends 'query' | 'json' | 'form' | 'header' | 'param' | 'cookie',
|
||||
Schema extends ZodObject
|
||||
T extends 'query' | 'json' | 'form' | 'header' | 'param' | 'cookie',
|
||||
S extends ZodSchema
|
||||
>(
|
||||
target: Target,
|
||||
schema: Schema
|
||||
target: T,
|
||||
schema: S
|
||||
) => {
|
||||
return zValidator(target, schema) as MiddlewareHandler<
|
||||
HonoEnv,
|
||||
string,
|
||||
{
|
||||
in: { [K in Target]: z.input<Schema> };
|
||||
out: { [K in Target]: z.output<Schema> };
|
||||
in: { [K in T]: z.input<S> };
|
||||
out: { [K in T]: z.output<S> };
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user