This commit is contained in:
2025-09-03 14:32:02 -04:00
parent c6e2b6e4d3
commit 03b751dda6
22 changed files with 1429 additions and 150 deletions

View File

@@ -37,6 +37,7 @@ interface MessageBase {
export class ProfileMessageManager extends ProfileContentManager {
// deno-lint-ignore require-await
async getMessages() {
return [];
}

View File

@@ -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(),

View File

@@ -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;
}

View File

@@ -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');
}
}

View File

@@ -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 });
}
}

View 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
}
}[]
}
}

View File

@@ -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";

View 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
}

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -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];