forked from zombieb/galvanic-corrosion-rewrite
duhhhhhhhh
This commit is contained in:
@@ -1,2 +1,20 @@
|
||||
import type Profile from "../profiles/profile.ts";
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
export type CommandExec = (...args: any[]) => unknown | Promise<unknown>;
|
||||
export type CommandExec = (...args: any[]) => unknown | Promise<unknown>;
|
||||
|
||||
export enum CommandSenderType {
|
||||
Console,
|
||||
Profile
|
||||
}
|
||||
export interface CommandSenderBase {
|
||||
type: CommandSenderType
|
||||
}
|
||||
export interface ConsoleSender extends CommandSenderBase {
|
||||
type: CommandSenderType.Console
|
||||
}
|
||||
export interface ProfileSender extends CommandSenderBase {
|
||||
type: CommandSenderType.Profile,
|
||||
prof: Profile
|
||||
}
|
||||
export type CommandSender = ConsoleSender | ProfileSender;
|
||||
@@ -34,6 +34,10 @@ export default class Command {
|
||||
return this.exec(...args);
|
||||
else if (!root) return new Error('No execution target for this root');
|
||||
|
||||
if (root === 'help') return JSON.stringify(this.subCmds.values()
|
||||
.map(cmd => cmd.getKey()).toArray()
|
||||
.reduce((prev, accumulator) => prev.concat(accumulator), []));
|
||||
|
||||
const cmd = this.subCmds.find(cmd => cmd.getKey().includes(root));
|
||||
if (cmd) {
|
||||
const newArgs = args.slice(1);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import { CommandSender, CommandSenderType } from "./cmdtypes.ts";
|
||||
import type Command from "./command.ts";
|
||||
|
||||
export class CommandsBase extends ServerContentBase {
|
||||
@@ -18,12 +19,20 @@ export class CommandsBase extends ServerContentBase {
|
||||
this.#cmds.delete(cmd);
|
||||
}
|
||||
|
||||
async dispatch(...args: string[]): Promise<unknown> {
|
||||
async dispatch(sender: CommandSender, ...args: string[]): Promise<unknown> {
|
||||
if (sender.type == CommandSenderType.Profile)
|
||||
if (await sender.prof.getRole() !== "developer") return new Error("Unauthorized");
|
||||
|
||||
const root = args[0];
|
||||
if (typeof root !== 'string') return new Error("Root command must be of primitive type 'string'");
|
||||
else {
|
||||
if (root === "help") return JSON.stringify(this.#cmds.values()
|
||||
.map(cmd => cmd.getKey()).toArray()
|
||||
.reduce((prev, accumulator) => prev.concat(accumulator), []));
|
||||
|
||||
const cmd = this.#cmds.values().toArray().find(cmd => cmd.getKey().includes(root));
|
||||
if (cmd) {
|
||||
|
||||
const val = await cmd.dispatch(...args.slice(1));
|
||||
if (val == null) return "null";
|
||||
else if (typeof val == 'string') return `"${val}"`;
|
||||
|
||||
29
src/server/consumables/base.ts
Normal file
29
src/server/consumables/base.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import path from "node:path";
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import { Consumable, ConsumableCollectionRuntimeConfig } from "./types.ts";
|
||||
import { RootPath } from "../../util/path.ts";
|
||||
import { PlatformMask } from "../platforms/types.ts";
|
||||
|
||||
export class ServerConsumablesBase extends ServerContentBase {
|
||||
|
||||
#consumableConfig: ConsumableCollectionRuntimeConfig | null = null;
|
||||
|
||||
protected override start() {
|
||||
this.#consumableConfig = JSON.parse(Deno.readTextFileSync(path.join(RootPath, '/res/consumables.json')));
|
||||
}
|
||||
|
||||
getAllDev(): Consumable[] {
|
||||
if (this.#consumableConfig == null) return [];
|
||||
else return this.#consumableConfig.consumablePrefabData.map(cons => ({
|
||||
Id: this.#consumableConfig!.consumablePrefabData.indexOf(cons),
|
||||
ConsumableItemDesc: cons.consumableName,
|
||||
PlatformMask: this.server.generateMask(PlatformMask.All),
|
||||
CreatedAt: new Date().toISOString(),
|
||||
Count: 99999999,
|
||||
InitialCount: 99999999,
|
||||
UnlockedLevel: 1,
|
||||
IsActive: false
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
19
src/server/consumables/types.ts
Normal file
19
src/server/consumables/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface ConsumablePrefab {
|
||||
fileName: string,
|
||||
consumableName: string
|
||||
}
|
||||
export interface ConsumableCollectionRuntimeConfig {
|
||||
consumablePrefabData: ConsumablePrefab[]
|
||||
}
|
||||
|
||||
export interface Consumable {
|
||||
Id: number,
|
||||
ConsumableItemDesc: string,
|
||||
PlatformMask: number,
|
||||
CreatedAt: string,
|
||||
Count: number,
|
||||
InitialCount: number,
|
||||
UnlockedLevel: number,
|
||||
IsActive: boolean,
|
||||
ActiveDurationMinutes?: number
|
||||
}
|
||||
@@ -1,45 +1,104 @@
|
||||
import { CloudRegionCode } from "../../util/photon.ts";
|
||||
import type Profile from "../profiles/profile.ts";
|
||||
import { RoomFactory } from "../rooms/internal/RoomFactory.ts";
|
||||
import { SubroomFactory } from "../rooms/internal/SubroomFactory.ts";
|
||||
import { type ServerBase } from "../server.ts";
|
||||
import { RoomInstance, RoomLocation } from "./types.ts";
|
||||
|
||||
export interface InstanceCreationOptions {
|
||||
roomId: number,
|
||||
subRoomId: number,
|
||||
room: RoomFactory,
|
||||
subroom: SubroomFactory,
|
||||
name: string,
|
||||
maxCapacity: number,
|
||||
private?: boolean
|
||||
private?: boolean,
|
||||
eventId?: number
|
||||
}
|
||||
|
||||
export class Instance {
|
||||
|
||||
#createdAt = new Date();
|
||||
|
||||
#players: Set<Profile> = new Set();
|
||||
#server: ServerBase;
|
||||
#init: boolean = true;
|
||||
|
||||
#instanceId: number;
|
||||
#roomId: number;
|
||||
#subRoomId: number;
|
||||
#location: RoomLocation;
|
||||
#name: string;
|
||||
#maxCapacity: number;
|
||||
#roomId: number = -1;
|
||||
#subRoomId: number = -1;
|
||||
#location: RoomLocation = RoomLocation.MakerRoom;
|
||||
#name: string = "Uninitialized Instance";
|
||||
#maxCapacity: number = 8;
|
||||
#isFull: boolean = false;
|
||||
#isPrivate: boolean;
|
||||
#isPrivate: boolean = false;
|
||||
#isInProgress: boolean = false;
|
||||
#photonRegionId: string = CloudRegionCode.us;
|
||||
#photonRoomId: string;
|
||||
#photonRoomId: string = "uninit";
|
||||
#dataBlob?: string;
|
||||
#eventId?: number
|
||||
#eventId?: number;
|
||||
|
||||
constructor(id: number, location: RoomLocation, options: InstanceCreationOptions) {
|
||||
get instanceId() { return this.#instanceId }
|
||||
|
||||
get roomId() { return this.#roomId }
|
||||
set roomId(data) { this.#roomId = data }
|
||||
|
||||
get subRoomId() { return this.#subRoomId }
|
||||
set subRoomId(data) { this.#subRoomId = data }
|
||||
|
||||
get location() { return this.#location }
|
||||
set location(data) { this.#location = data }
|
||||
|
||||
get name() { return this.#name }
|
||||
set name(data) { this.#name = data }
|
||||
|
||||
get maxCapacity() { return this.#maxCapacity }
|
||||
set maxCapacity(data) { this.#maxCapacity = data }
|
||||
|
||||
get isFull() { return this.#isFull }
|
||||
set isFull(data) { this.#isFull = data }
|
||||
|
||||
get isPrivate() { return this.#isPrivate }
|
||||
set isPrivate(data) { this.#isPrivate = data }
|
||||
|
||||
get isInProgress() { return this.#isInProgress }
|
||||
set isInProgress(data) { this.#isInProgress = data }
|
||||
|
||||
get photonRegionId() { return this.#photonRegionId }
|
||||
set photonRegionId(data) { this.#photonRegionId = data }
|
||||
|
||||
get photonRoomId() { return this.#photonRoomId }
|
||||
set photonRoomId(data) { this.#photonRoomId = data }
|
||||
|
||||
get dataBlob() { return this.#dataBlob }
|
||||
set dataBlob(data) { this.#dataBlob = data }
|
||||
|
||||
get eventId() { return this.#eventId }
|
||||
set eventId(data) { this.#eventId = data }
|
||||
|
||||
supportsJoinInProgress: boolean;
|
||||
|
||||
constructor(server: ServerBase, id: number, options: InstanceCreationOptions) {
|
||||
this.#instanceId = id;
|
||||
this.#location = location;
|
||||
this.location = options.subroom.RoomSceneLocationId;
|
||||
this.#server = server;
|
||||
|
||||
this.#roomId = options.roomId;
|
||||
this.#subRoomId = options.subRoomId;
|
||||
this.#isPrivate = typeof options.private == 'boolean' ? options.private : false;
|
||||
this.#name = options.name;
|
||||
this.#maxCapacity = options.maxCapacity;
|
||||
this.#photonRoomId = `GCR-${this.#instanceId}`;
|
||||
this.roomId = options.room.getRoomId();
|
||||
this.subRoomId = options.subroom.RoomSceneId;
|
||||
this.isPrivate = typeof options.private == 'boolean' ? options.private : false;
|
||||
this.name = options.name;
|
||||
this.maxCapacity = options.subroom.MaxPlayers;
|
||||
this.photonRoomId = `GCR-${this.instanceId}`;
|
||||
|
||||
this.supportsJoinInProgress = server.Rooms.sceneSupportsJoinInProgress(options.room, options.subroom);
|
||||
|
||||
this.#init = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be heavy, so promise is used
|
||||
*/
|
||||
// deno-lint-ignore require-await
|
||||
async #multiPresenceUpdate() {
|
||||
if (this.#init) return;
|
||||
for (const prof of this.#players)
|
||||
prof.getSocketHandler()?.sendNotification("PresenceUpdate", this.#server.Presence.getPresence(prof).export());
|
||||
}
|
||||
|
||||
getPlayers() {
|
||||
@@ -61,21 +120,26 @@ export class Instance {
|
||||
|
||||
export() {
|
||||
const inst: RoomInstance = {
|
||||
roomInstanceId: this.#instanceId,
|
||||
roomId: this.#roomId,
|
||||
subRoomId: this.#subRoomId,
|
||||
location: this.#location,
|
||||
name: this.#name,
|
||||
maxCapacity: this.#maxCapacity,
|
||||
isFull: this.#isFull,
|
||||
isPrivate: this.#isPrivate,
|
||||
isInProgress: this.#isInProgress,
|
||||
photonRegionId: this.#photonRegionId,
|
||||
photonRoomId: this.#photonRoomId,
|
||||
dataBlob: this.#dataBlob,
|
||||
eventId: this.#eventId
|
||||
roomInstanceId: this.instanceId,
|
||||
roomId: this.roomId,
|
||||
subRoomId: this.subRoomId,
|
||||
location: this.location,
|
||||
name: this.name,
|
||||
maxCapacity: this.maxCapacity,
|
||||
isFull: this.isFull,
|
||||
isPrivate: this.isPrivate,
|
||||
isInProgress: this.isInProgress,
|
||||
photonRegionId: this.photonRegionId,
|
||||
photonRoomId: this.photonRoomId,
|
||||
dataBlob: this.dataBlob,
|
||||
eventId: this.eventId
|
||||
};
|
||||
return inst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current player count (instance size)
|
||||
*/
|
||||
get size() { return this.getPlayers().length }
|
||||
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import { type Instance } from "./Instance.ts";
|
||||
import { Instance, InstanceCreationOptions } from "./Instance.ts";
|
||||
import Command from "../commands/command.ts";
|
||||
import z from "zod";
|
||||
import { PushNotificationId } from "../socket/signalr/types.ts";
|
||||
|
||||
const log = new Logging("Instances");
|
||||
|
||||
@@ -8,40 +11,80 @@ export class InstanceManager extends ServerContentBase {
|
||||
|
||||
#instances: Set<Instance> = new Set();
|
||||
|
||||
clearEmptyInstances() {
|
||||
protected override start() {
|
||||
this.server.Commands.addRootCommand(new Command({
|
||||
key: ["inst", "i", "instance", "instances"],
|
||||
subcommands: [
|
||||
new Command({
|
||||
key: ["getall", "all", "fetchall", "list"],
|
||||
exec: () => {
|
||||
return this.#instances.values().toArray().map(inst => inst.export());
|
||||
},
|
||||
zod: z.tuple([]),
|
||||
help: "Get all instances"
|
||||
}),
|
||||
new Command({
|
||||
key: ["kicklive", "modkick", "quitgame"],
|
||||
exec: (id: number) => {
|
||||
const inst = this.server.Instances.getInstance(id);
|
||||
if (!inst) return false;
|
||||
|
||||
inst.getPlayers().forEach(prof => prof.getSocketHandler()?.sendNotification(PushNotificationId.ModerationKick));
|
||||
return true;
|
||||
},
|
||||
zod: z.tuple([z.coerce.number()]),
|
||||
help: "Send ModerationKick to all players in an instance. Returns true if successful."
|
||||
})
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be heavy if instance count is high.
|
||||
*/
|
||||
// deno-lint-ignore require-await
|
||||
async clearEmptyInstances() {
|
||||
log.i(`Starting instance purge\n Before: ${
|
||||
this.#instances.size
|
||||
} instances, ${
|
||||
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
|
||||
} players`);
|
||||
|
||||
return new Promise(() => {
|
||||
for (const inst of this.#instances) {
|
||||
if (inst.getPlayers().length === 0) this.deleteInstance(inst);
|
||||
}
|
||||
for (const inst of this.#instances)
|
||||
if (inst.getPlayers().length === 0) this.deleteInstance(inst);
|
||||
|
||||
|
||||
log.i(`Instance purge complete\n After: ${
|
||||
this.#instances.size
|
||||
} instances, ${
|
||||
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
|
||||
} players`);
|
||||
});
|
||||
log.i(`Instance purge complete\n After: ${
|
||||
this.#instances.size
|
||||
} instances, ${
|
||||
this.#instances.values().reduce((prev, current) => prev + current.getPlayers().length, 0)
|
||||
} players`);
|
||||
}
|
||||
|
||||
getAllInstances() {
|
||||
|
||||
return this.#instances;
|
||||
}
|
||||
|
||||
registerInstance(inst: Instance) {
|
||||
|
||||
#generateAvailableId() {
|
||||
let id = Math.round(Math.random() * Math.pow(2, 31));
|
||||
while (this.#instances.values().find(inst => inst.instanceId === id)) id = this.#generateAvailableId();
|
||||
return id;
|
||||
}
|
||||
#registerInstance(inst: Instance) {
|
||||
if (!this.#instances.has(inst)) this.#instances.add(inst);
|
||||
}
|
||||
createInstance(options: InstanceCreationOptions) {
|
||||
const inst = new Instance(this.server, this.#generateAvailableId(), options);
|
||||
this.#instances.add(inst);
|
||||
return inst;
|
||||
}
|
||||
|
||||
getInstance(id: number) {
|
||||
|
||||
return this.#instances.values().find(inst => inst.instanceId === id);
|
||||
}
|
||||
|
||||
deleteInstance(inst: Instance) {
|
||||
|
||||
this.#instances.delete(inst);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type Instance } from "./Instance.ts";
|
||||
|
||||
export enum RoomLocation {
|
||||
Calibration = "f5fbd9c9-e853-4036-9d48-5f68e861af04",
|
||||
DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163",
|
||||
@@ -52,4 +54,8 @@ export interface RoomInstance {
|
||||
photonRoomId: string;
|
||||
dataBlob?: string;
|
||||
eventId?: number;
|
||||
}
|
||||
|
||||
export interface InstanceUpdatedEvent {
|
||||
instance: Instance
|
||||
}
|
||||
@@ -1,7 +1,121 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import { Instance } from "../instances/Instance.ts";
|
||||
import { type RoomInstance } from "../instances/types.ts";
|
||||
import type Profile from "../profiles/profile.ts";
|
||||
import { RoomDataTypes } from "../rooms/internal/RoomDataTypes.ts";
|
||||
import { type RoomFactory } from "../rooms/internal/RoomFactory.ts";
|
||||
import { MatchmakingErrorCode } from "./types.ts";
|
||||
|
||||
const log = new Logging("Matchmaking");
|
||||
|
||||
export interface MatchmakingOptions {
|
||||
roomName: string,
|
||||
subRoomName?: string,
|
||||
private?: boolean,
|
||||
instanceId?: number,
|
||||
profile: Profile
|
||||
}
|
||||
|
||||
export interface InternalMatchmakingResponse {
|
||||
errorCode: MatchmakingErrorCode,
|
||||
roomInstance?: Instance
|
||||
}
|
||||
export interface MatchmakingResponse {
|
||||
errorCode: MatchmakingErrorCode,
|
||||
roomInstance?: RoomInstance
|
||||
}
|
||||
|
||||
export class ServerMatchmakingBase extends ServerContentBase {
|
||||
|
||||
|
||||
async matchmake(options: MatchmakingOptions): Promise<InternalMatchmakingResponse | null> {
|
||||
|
||||
function getInstanceName() {
|
||||
if (options.roomName === 'DormRoom')
|
||||
return `@${options.profile.getUsername()}'s Dorm`;
|
||||
else {
|
||||
if (options.subRoomName || options.subRoomName !== "Home") return `^${options.roomName}.${options.subRoomName}`;
|
||||
else return `^${options.roomName}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.instanceId) {
|
||||
|
||||
const instance = this.server.Instances.getInstance(options.instanceId);
|
||||
if (instance) {
|
||||
|
||||
if (instance.hasPlayer(options.profile)) return { errorCode: MatchmakingErrorCode.AlreadyInTargetInstance }
|
||||
else {
|
||||
if (instance.isFull) return { errorCode: MatchmakingErrorCode.InsufficientSpace }
|
||||
else if (instance.isPrivate) return { errorCode: MatchmakingErrorCode.RoomInstanceIsPrivate }
|
||||
|
||||
options.profile.updateInstance(instance);
|
||||
await this.server.Instances.clearEmptyInstances();
|
||||
|
||||
return { errorCode: MatchmakingErrorCode.Success, roomInstance: instance };
|
||||
}
|
||||
|
||||
} else return { errorCode: MatchmakingErrorCode.NoSuchGame };
|
||||
|
||||
} else {
|
||||
|
||||
let targetRoom: RoomFactory | null = null;
|
||||
if (options.roomName == 'DormRoom') targetRoom = await this.server.Rooms.getPlayerDorm(options.profile);
|
||||
else targetRoom = await this.server.Rooms.getByName(options.roomName);
|
||||
|
||||
if (!targetRoom) return { errorCode: MatchmakingErrorCode.NoSuchRoom };
|
||||
if (targetRoom.Accessibility == RoomDataTypes.RoomAccessibility.Private && targetRoom.CreatorPlayerId !== options.profile.getId())
|
||||
return { errorCode: MatchmakingErrorCode.RoomIsPrivate };
|
||||
if (targetRoom.State !== RoomDataTypes.RoomState.Active) return { errorCode: MatchmakingErrorCode.RoomIsNotActive };
|
||||
|
||||
await this.server.Instances.clearEmptyInstances();
|
||||
|
||||
let allInstances = this.server.Instances.getAllInstances();
|
||||
const subroom = (await targetRoom.getAllSubrooms()).values().find(factory => factory.Name == options.subRoomName);
|
||||
|
||||
// if a subroom was specified, filter instances that
|
||||
if (subroom) allInstances = new Set(allInstances.values().filter(inst => inst.subRoomId == subroom.RoomSceneId));
|
||||
|
||||
// filter out instances that are in progress and do not support joininprogress
|
||||
allInstances = new Set(allInstances.values().filter(inst => !(inst.isInProgress && !inst.supportsJoinInProgress)));
|
||||
|
||||
// sort instances
|
||||
allInstances = new Set(allInstances.values().toArray().sort((a, b) => a.size + b.size));
|
||||
|
||||
const foundInstance = allInstances.values().toArray()[0];
|
||||
if (!foundInstance) {
|
||||
|
||||
const matchmakeableSubrooms = (await targetRoom.getAllSubrooms()).values().filter(scene => scene.CanMatchmakeInto).toArray();
|
||||
const index = Math.floor(Math.random() * matchmakeableSubrooms.length);
|
||||
|
||||
log.d(`Scene ${matchmakeableSubrooms[index].RoomSceneId} was chosen for matchmaking into new instance of room ${targetRoom.getRoomId()}`);
|
||||
const newInst = this.server.Instances.createInstance({
|
||||
room: targetRoom,
|
||||
subroom: matchmakeableSubrooms[index],
|
||||
private: options.private,
|
||||
name: getInstanceName()
|
||||
});
|
||||
|
||||
options.profile.updateInstance(newInst);
|
||||
this.server.Instances.clearEmptyInstances();
|
||||
|
||||
return { errorCode: MatchmakingErrorCode.Success, roomInstance: newInst };
|
||||
|
||||
} else {
|
||||
|
||||
const currentInst = options.profile.getInstance();
|
||||
if (currentInst?.instanceId === foundInstance.instanceId)
|
||||
return { errorCode: MatchmakingErrorCode.AlreadyInBestInstance };
|
||||
|
||||
options.profile.updateInstance(foundInstance);
|
||||
this.server.Instances.clearEmptyInstances();
|
||||
|
||||
return { errorCode: MatchmakingErrorCode.Success, roomInstance: foundInstance };
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
enum MatchmakingErrorCode {
|
||||
export enum MatchmakingErrorCode {
|
||||
Success,
|
||||
NoSuchGame,
|
||||
PlayerNotOnline,
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import z from "zod";
|
||||
import Command from "../commands/command.ts";
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import { transformCheckEnum, transformStringToEnum } from "../../util/validators.ts";
|
||||
import { transformCheckEnum } from "../../util/validators.ts";
|
||||
import { sign } from "@hono/hono/jwt";
|
||||
import { CachedLogin, DbCachedLogin, PlatformMask, PlatformType, TokenFormat, TokenType } from "./types.ts";
|
||||
import type Profile from "../profiles/profile.ts";
|
||||
import { getNetConfig } from "../../net.ts";
|
||||
|
||||
export const steamAuthTicketSchema = z.object({
|
||||
Ticket: z.string().min(256),
|
||||
AppId: z.literal("471710")
|
||||
});
|
||||
|
||||
const netConfig = getNetConfig();
|
||||
|
||||
export class PlatformsManager extends ServerContentBase {
|
||||
|
||||
static platformsKey = "platforms";
|
||||
@@ -18,15 +22,18 @@ export class PlatformsManager extends ServerContentBase {
|
||||
return [PlatformsManager.platformsKey, ...keys.filter(val => typeof val == 'string')];
|
||||
}
|
||||
|
||||
async getToken(accountId: number, type: TokenType) {
|
||||
async getToken(prof: Profile, type: TokenType) {
|
||||
const secret = Deno.env.get('SECRET');
|
||||
if (!secret) throw new Error("No SECRET in env. Did you forget to set it?");
|
||||
|
||||
const exp = type == TokenType.Access ? Math.round(Date.now() / 1000) + 21_600 : Math.round(Date.now() / 1000) + 31_556_952;
|
||||
|
||||
const token: TokenFormat = {
|
||||
typ: type,
|
||||
sub: accountId,
|
||||
iss: "https://yarns.proxnet.dev/auth/",
|
||||
exp: type == TokenType.Access ? Math.round(Date.now() / 1000) + 21_600 : Math.round(Date.now() / 1000) + 31_556_952
|
||||
sub: prof.getId(),
|
||||
role: await prof.getRole(),
|
||||
iss: `${netConfig.securePublicHost ? "https" : "http"}://${netConfig.publicHost}/auth`,
|
||||
iat: Math.round(Date.now() / 1000) - 5,
|
||||
exp
|
||||
}
|
||||
return await sign(JSON.parse(JSON.stringify(token)), secret);
|
||||
}
|
||||
@@ -138,9 +145,9 @@ export class PlatformsManager extends ServerContentBase {
|
||||
return await this.addCachedLogin(type, platformId, accountId);
|
||||
},
|
||||
zod: z.tuple([
|
||||
z.string().transform(transformStringToEnum<PlatformType>(PlatformType)),
|
||||
z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
|
||||
z.string(),
|
||||
z.string().transform(Number)
|
||||
z.coerce.number()
|
||||
]),
|
||||
help: 'Add a cachedlogin for platformId: <type: PlatformType>, <platformId: string>, <accountId: number>'
|
||||
}),
|
||||
@@ -150,9 +157,9 @@ export class PlatformsManager extends ServerContentBase {
|
||||
return await this.deleteCachedLogin(type, platformId, accountId);
|
||||
},
|
||||
zod: z.tuple([
|
||||
z.string().transform(transformStringToEnum<PlatformType>(PlatformType)),
|
||||
z.coerce.number().transform(transformCheckEnum<PlatformType>(PlatformType)),
|
||||
z.string(),
|
||||
z.string().transform(Number)
|
||||
z.coerce.number()
|
||||
]),
|
||||
help: 'Remove a cachedlogin for platformId: <type: PlatformType>, <platformId: string>, <accountId: number>'
|
||||
})
|
||||
|
||||
@@ -8,14 +8,13 @@ export interface TokenFormatBase {
|
||||
export interface TokenFormat extends TokenFormatBase {
|
||||
iss: string,
|
||||
exp: number,
|
||||
iat: number,
|
||||
sub: number,
|
||||
role: ProfileRole
|
||||
}
|
||||
|
||||
export enum ProfileRole {
|
||||
Developer = 'developer',
|
||||
Web = 'webClient',
|
||||
Game = 'gameClient'
|
||||
}
|
||||
export const ProfileRole = ["developer", "gameClient", "webClient"] as const;
|
||||
export type ProfileRole = typeof ProfileRole[number];
|
||||
|
||||
export enum PlatformType {
|
||||
All = -1,
|
||||
|
||||
@@ -4,6 +4,9 @@ import { DeviceClass } from "../platforms/types.ts";
|
||||
import Profile from "../profiles/profile.ts";
|
||||
import { type ServerBase } from "../server.ts";
|
||||
import { RoomInstance } from "../instances/types.ts";
|
||||
import Command from "../commands/command.ts";
|
||||
import z from "zod";
|
||||
import { PushNotificationId } from "../socket/signalr/types.ts";
|
||||
|
||||
export enum VRMovementMode {
|
||||
TELEPORT,
|
||||
@@ -33,7 +36,6 @@ export class Presence {
|
||||
|
||||
#statusVisibility: PlayerStatusVisibility = PlayerStatusVisibility.Offline;
|
||||
#deviceClass: DeviceClass = DeviceClass.Unknown;
|
||||
#roomInstance: RoomInstance | null = null;
|
||||
#vrMovementMove: VRMovementMode | undefined;
|
||||
|
||||
#lastExported: Date = new Date();
|
||||
@@ -63,12 +65,16 @@ export class Presence {
|
||||
this.#statusVisibility = sv;
|
||||
this.updateLastSeen();
|
||||
this.update();
|
||||
|
||||
this.#server.emit('presence.update', { profile: this.#profile, presence: this });
|
||||
}
|
||||
|
||||
setVRMovementMode(mm: VRMovementMode) {
|
||||
this.#vrMovementMove = mm;
|
||||
this.updateLastSeen();
|
||||
this.update();
|
||||
|
||||
this.#server.emit('presence.update', { profile: this.#profile, presence: this });
|
||||
}
|
||||
|
||||
getLastExported() {
|
||||
@@ -87,14 +93,14 @@ export class Presence {
|
||||
statusVisibility: this.#statusVisibility,
|
||||
deviceClass: this.#deviceClass,
|
||||
vrMovementMode: this.#vrMovementMove,
|
||||
roomInstance: this.#roomInstance
|
||||
roomInstance: this.#profile.getInstance()?.export() ?? null
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PresenceBase extends ServerContentBase {
|
||||
export class ServerPresenceBase extends ServerContentBase {
|
||||
|
||||
#log = new Logging("Presence");
|
||||
|
||||
@@ -119,9 +125,32 @@ export class PresenceBase extends ServerContentBase {
|
||||
|
||||
override start() {
|
||||
this.#intervalId = setInterval(() => {
|
||||
if (this.#presenceMap.size === 0) return;
|
||||
|
||||
this.#log.i('Clearing dead presences');
|
||||
this.#deleteDeadPresences();
|
||||
}, 300_000);
|
||||
|
||||
this.server.Commands.addRootCommand(new Command({
|
||||
key: ["presence", "pres"],
|
||||
subcommands: [
|
||||
new Command({
|
||||
key: ["quit", "quitgame"],
|
||||
exec: async (pid: number) => {
|
||||
const prof = await this.server.Profiles.get(pid);
|
||||
if (!prof) return false;
|
||||
|
||||
const socket = prof.getSocketHandler();
|
||||
if (!socket) return false;
|
||||
|
||||
socket.sendNotification(PushNotificationId.ModerationQuitGame);
|
||||
return true;
|
||||
},
|
||||
zod: z.tuple([z.coerce.number()]),
|
||||
help: "Sends ModerationQuitGame to a player's socket if it is connected. Returns true if successful."
|
||||
})
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
|
||||
@@ -4,7 +4,7 @@ import Profile from "./profile.ts";
|
||||
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 { PlatformMask, PlatformType, ProfileRole, TokenType } from "../platforms/types.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
|
||||
const profiles: Map<number, Profile> = new Map();
|
||||
@@ -116,7 +116,7 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
else return prof.export();
|
||||
},
|
||||
zod: z.tuple([
|
||||
z.string().transform(Number)
|
||||
z.coerce.number()
|
||||
]),
|
||||
help: 'Fetch a profile: <id: number>'
|
||||
}),
|
||||
@@ -137,9 +137,9 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
else return await profile.getRole();
|
||||
},
|
||||
zod: z.tuple([
|
||||
z.string().transform(Number)
|
||||
z.coerce.number()
|
||||
]),
|
||||
help: 'Set the profile role: <id: number>'
|
||||
help: 'Get profile role: <id: number>'
|
||||
}),
|
||||
new Command({
|
||||
key: ['setrole', 'sr'],
|
||||
@@ -150,9 +150,9 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
},
|
||||
zod: z.tuple([
|
||||
z.coerce.number(),
|
||||
z.string()
|
||||
z.literal(ProfileRole)
|
||||
]),
|
||||
help: 'Set the profile role: <id: number, role: "gameClient" | "webClient" | "developer">'
|
||||
help: 'Set profile role: <id: number, role: "gameClient" | "webClient" | "developer">'
|
||||
}),
|
||||
new Command({
|
||||
key: ['settings', 'setting'],
|
||||
@@ -163,6 +163,16 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
},
|
||||
zod: z.tuple([z.coerce.number()]),
|
||||
help: "Get player settings"
|
||||
}),
|
||||
new Command({
|
||||
key: ["refreshtoken", "simutoken"],
|
||||
exec: async (id: number) => {
|
||||
const profile = await this.get(id);
|
||||
if (profile) return await this.server.Platforms.getToken(profile, TokenType.Refresh);
|
||||
else return null;
|
||||
},
|
||||
zod: z.tuple([z.coerce.number()]),
|
||||
help: "Get a profile's refresh token / simulate generation of refresh token"
|
||||
})
|
||||
]
|
||||
}));
|
||||
|
||||
@@ -59,15 +59,15 @@ class Profile {
|
||||
}
|
||||
|
||||
constructProfilePropertyKey(...keys: (string | undefined)[]) {
|
||||
return [ ProfileManagerBase.profilesKey, this.#id, ...keys.filter(val => typeof val == 'string') ];
|
||||
return [ProfileManagerBase.profilesKey, this.#id, ...keys.filter(val => typeof val == 'string')];
|
||||
}
|
||||
|
||||
getUsername() {
|
||||
return this.#selfAcc.username;
|
||||
}
|
||||
async setUsername(username: string) {
|
||||
this.#kv.getKv().delete([ ProfileManagerBase.profilesKey, this.#selfAcc.username ]);
|
||||
this.#kv.getKv().set([ ProfileManagerBase.profilesKey, username ], this.getId());
|
||||
this.#kv.getKv().delete([ProfileManagerBase.profilesKey, this.#selfAcc.username]);
|
||||
this.#kv.getKv().set([ProfileManagerBase.profilesKey, username], this.getId());
|
||||
|
||||
this.#selfAcc.username = username;
|
||||
await this.#saveSelfAcc();
|
||||
@@ -81,7 +81,7 @@ class Profile {
|
||||
await this.#saveSelfAcc();
|
||||
}
|
||||
|
||||
async getBio(){
|
||||
async getBio() {
|
||||
const key = this.constructProfilePropertyKey('bio');
|
||||
const val = await this.#kv.getKv().get<string>(key);
|
||||
if (!val.value) return null;
|
||||
@@ -136,7 +136,17 @@ class Profile {
|
||||
getInstance() {
|
||||
return this.#instance;
|
||||
}
|
||||
setInstance(inst: Instance) {
|
||||
updateInstance(inst: Instance | null) {
|
||||
if (inst == null) {
|
||||
if (this.#instance) this.#instance.removePlayer(this);
|
||||
this.#instance = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#instance) this.#instance.removePlayer(this);
|
||||
inst.addPlayer(this);
|
||||
|
||||
this.#server.emit('presence.update', { profile: this, presence: this.#server.Presence.getPresence(this) });
|
||||
this.#instance = inst;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,13 @@ import { AGRoom, AGRoomLocation, AGRoomRuntimeConfig } from "./internal/ClientRo
|
||||
import Command from "../commands/command.ts";
|
||||
import z from "zod";
|
||||
import { RoomLocation } from "../instances/types.ts";
|
||||
import { RootPath } from "../../util/path.ts";
|
||||
import path from "node:path";
|
||||
import { SubroomFactory } from "./internal/SubroomFactory.ts";
|
||||
|
||||
export const roomNameSchema = z.string().min(4).max(128).regex(/^[A-Za-z0-9._-]+$/);
|
||||
export const roomIdSchema = z.coerce.number().min(1).max(Math.pow(2, 31));
|
||||
|
||||
const roomIdSchema = z.coerce.number().min(1).max(Math.pow(2, 31));
|
||||
export class ServerRoomsBase extends ServerContentBase {
|
||||
|
||||
#subroomKv = new KV('subrooms', true);
|
||||
@@ -21,19 +26,24 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
static roomNamesKey = "room_names";
|
||||
static playerDormsKey = "dorms";
|
||||
|
||||
#agrooms: Set<number> = new Set();
|
||||
#baserooms: Set<number> = new Set();
|
||||
#agroomIds: Set<number> = new Set();
|
||||
#baseroomIds: Set<number> = new Set();
|
||||
#agRoomRuntimeConfig: AGRoomRuntimeConfig | null = null;
|
||||
#joinInProgressLookup: Record<RoomLocation, boolean> | null = null;
|
||||
|
||||
override async start() {
|
||||
await this.#subroomKv.init();
|
||||
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`);
|
||||
if (agrooms.value !== null) this.#agroomIds = agrooms.value;
|
||||
this.#log.i(`${this.#agroomIds.size} AG rooms exist`);
|
||||
|
||||
const baserooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.baseRoomIdsKey]);
|
||||
if (baserooms.value !== null) this.#baserooms = baserooms.value;
|
||||
if (baserooms.value !== null) this.#baseroomIds = baserooms.value;
|
||||
|
||||
this.#agRoomRuntimeConfig = JSON.parse(Deno.readTextFileSync(path.join(RootPath, "/res/rooms.json")));
|
||||
this.#joinInProgressLookup = JSON.parse(Deno.readTextFileSync(path.join(RootPath, "/res/staticJoinInProgressLookup.json")));
|
||||
|
||||
this.server.Commands.addRootCommand(new Command({
|
||||
key: ["rooms", "r", "room"],
|
||||
@@ -82,10 +92,17 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
}))
|
||||
}
|
||||
async #writeAgRooms() {
|
||||
await this.kv.getKv().set([ServerRoomsBase.agRoomIdsKey], this.#agrooms);
|
||||
await this.kv.getKv().set([ServerRoomsBase.agRoomIdsKey], this.#agroomIds);
|
||||
}
|
||||
async #writeBaseRooms() {
|
||||
await this.kv.getKv().set([ServerRoomsBase.baseRoomIdsKey], this.#baserooms);
|
||||
await this.kv.getKv().set([ServerRoomsBase.baseRoomIdsKey], this.#baseroomIds);
|
||||
}
|
||||
getAgRoomIds() {
|
||||
return this.#agroomIds;
|
||||
}
|
||||
getAgRoomRuntimeConfig() {
|
||||
if (!this.#agRoomRuntimeConfig) throw new Error("Config has not yet been initialized");
|
||||
return this.#agRoomRuntimeConfig;
|
||||
}
|
||||
|
||||
async initBuiltinRooms(rooms: AGRoom[], locations: AGRoomLocation[]) {
|
||||
@@ -98,7 +115,10 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
"ARRoom",
|
||||
"Registration",
|
||||
"DormRoom"
|
||||
].includes(room.Name)) return;
|
||||
].includes(room.Name)) {
|
||||
this.#log.w(`Room '${room.Name}' is not eligible for builtin room generation`);
|
||||
return;
|
||||
}
|
||||
|
||||
const roomFactory = await this.write();
|
||||
if (roomFactory == null) {
|
||||
@@ -118,7 +138,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
roomFactory.Description = room.Description;
|
||||
roomFactory.IsAGRoom = true;
|
||||
roomFactory.CloningAllowed = room.CloningAllowed;
|
||||
roomFactory.ImageName = `${room.Name}.png`
|
||||
roomFactory.ImageName = `${room.Name}.png`;
|
||||
|
||||
const supportPromises: Promise<unknown>[] = [];
|
||||
roomFactory.removeAllHardwareSupport();
|
||||
@@ -134,7 +154,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
|
||||
subroomFactory.RoomId = roomFactory.getRoomId();
|
||||
subroomFactory.Name = scene.Name;
|
||||
subroomFactory.RoomSceneLocationId = scene.RoomSceneLocationId;
|
||||
subroomFactory.RoomSceneLocationId = scene.RoomSceneLocationId as RoomLocation;
|
||||
subroomFactory.IsSandbox = scene.IsSandbox;
|
||||
subroomFactory.CanMatchmakeInto = scene.CanMatchmakeInto;
|
||||
subroomFactory.MaxPlayers = scene.MaxPlayers;
|
||||
@@ -146,15 +166,33 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
|
||||
await Promise.all(supportPromises);
|
||||
|
||||
this.#agrooms.add(roomFactory.getRoomId());
|
||||
this.#agroomIds.add(roomFactory.getRoomId());
|
||||
await roomFactory.write();
|
||||
|
||||
if (room.CloningAllowed) this.#baserooms.add(roomFactory.getRoomId());
|
||||
if (room.CloningAllowed) this.#baseroomIds.add(roomFactory.getRoomId());
|
||||
}));
|
||||
|
||||
await this.#writeAgRooms();
|
||||
await this.#writeBaseRooms();
|
||||
this.#log.i(`${this.#agrooms.size} AG rooms added: [${this.#agrooms.values().toArray().join(',')}]`);
|
||||
this.#log.i(`${this.#agroomIds.size} AG rooms added: [${this.#agroomIds.values().toArray().join(',')}]`);
|
||||
}
|
||||
|
||||
sceneSupportsJoinInProgress(roomFactory: RoomFactory, subroomFactory: SubroomFactory) {
|
||||
const agRoomRuntimeConfig = this.server.Rooms.getAgRoomRuntimeConfig();
|
||||
|
||||
const builtinScene = agRoomRuntimeConfig.Rooms.find(room => room.Name === roomFactory.Name)
|
||||
?.Scenes.find(scene => scene.RoomSceneLocationId === subroomFactory.RoomSceneLocationId);
|
||||
if (builtinScene) return builtinScene.SupportsJoinInProgress;
|
||||
else {
|
||||
if (!this.#joinInProgressLookup) throw new Error("JoinInProgress lookup table is not yet initialized");
|
||||
const lookup = this.#joinInProgressLookup[subroomFactory.RoomSceneLocationId];
|
||||
if (lookup) return lookup;
|
||||
else return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getMany(...ids: number[]) {
|
||||
return (await Promise.all(ids.map(id => this.get(id)))).filter(val => val !== null);
|
||||
}
|
||||
|
||||
async getAvailableRoomId() {
|
||||
@@ -163,6 +201,10 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
return id;
|
||||
}
|
||||
|
||||
async getByRoomSceneId(id: number) {
|
||||
return await new SubroomFactory(this.server, this.#subroomKv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
|
||||
}
|
||||
|
||||
getKv() {
|
||||
return this.kv;
|
||||
}
|
||||
@@ -176,7 +218,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
async getPlayerDorm(profile: Profile) {
|
||||
const id = await this.kv.getKv().get<number>([ServerRoomsBase.playerDormsKey, profile.getId()]);
|
||||
if (id.value == null) {
|
||||
const roomFactory = await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree, id: await this.getAvailableRoomId() });
|
||||
const roomFactory = await new RoomFactory(this.server, this.kv, this.#subroomKv).init({ mode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree, id: await this.getAvailableRoomId() });
|
||||
if (!roomFactory) return null;
|
||||
roomFactory.setRoomProperties({
|
||||
Name: `DormRoom`,
|
||||
@@ -216,7 +258,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
|
||||
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, this.#subroomKv).init({ mode: FactoryMode.Fetch, id: id.value });
|
||||
}
|
||||
|
||||
async getByName(name: string) {
|
||||
@@ -226,11 +268,11 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
}
|
||||
|
||||
async get(id: number) {
|
||||
return await new RoomFactory(this.server, this.kv).init({ mode: FactoryMode.Fetch, id: id });
|
||||
return await new RoomFactory(this.server, this.kv, this.#subroomKv).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() });
|
||||
return await new RoomFactory(this.server, this.kv, this.#subroomKv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: await this.getAvailableRoomId() });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { RoomLocation } from "../../instances/types.ts";
|
||||
|
||||
export enum WriteMode {
|
||||
Overwrite = "overwrite",
|
||||
WriteIfFree = "if_free"
|
||||
@@ -34,7 +36,7 @@ export enum RoomAccessibility {
|
||||
export interface RoomScene {
|
||||
RoomSceneId: number,
|
||||
RoomId: number,
|
||||
RoomSceneLocationId: string,
|
||||
RoomSceneLocationId: RoomLocation,
|
||||
Name: string,
|
||||
IsSandbox: boolean,
|
||||
DataBlobName: string,
|
||||
@@ -151,7 +153,7 @@ export interface DatabaseRoom {
|
||||
|
||||
export interface SubroomProps {
|
||||
RoomId: number,
|
||||
RoomSceneLocationId: string,
|
||||
RoomSceneLocationId: RoomLocation,
|
||||
Name: string,
|
||||
IsSandbox: boolean,
|
||||
MaxPlayers: number,
|
||||
|
||||
@@ -29,6 +29,7 @@ export class RoomFactory {
|
||||
|
||||
#server: ServerBase;
|
||||
#kv: KV;
|
||||
#subroomKv: KV;
|
||||
|
||||
#roomId: number | undefined;
|
||||
|
||||
@@ -43,10 +44,11 @@ export class RoomFactory {
|
||||
#cannotAccessBeforeInitError = new Error("Cannot access properties before initialization");
|
||||
#cannotWriteBeforeInitError = new Error("Cannot write before initialization");
|
||||
|
||||
constructor(server: ServerBase, kv: KV) {
|
||||
constructor(server: ServerBase, kv: KV, subroomKv: KV) {
|
||||
|
||||
this.#server = server;
|
||||
this.#kv = kv;
|
||||
this.#subroomKv = subroomKv;
|
||||
|
||||
}
|
||||
|
||||
@@ -161,9 +163,7 @@ export class RoomFactory {
|
||||
const autoTags = this.getTags();
|
||||
const galvTags = this.getGalvanicTags();
|
||||
|
||||
const subroomExports = (await Promise.all(
|
||||
this.getSubrooms().values().map(subroom => this.getSubroom(subroom))
|
||||
)).map(factory => factory.export());
|
||||
const subroomExports = (await Promise.all((await this.getAllSubrooms()).values())).map(factory => factory.export());
|
||||
|
||||
return {
|
||||
Room: {
|
||||
@@ -208,22 +208,27 @@ export class RoomFactory {
|
||||
return this.#roomId;
|
||||
}
|
||||
|
||||
getSubrooms() {
|
||||
getSubroomIds() {
|
||||
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
||||
return this.#subrooms;
|
||||
}
|
||||
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.#subroomKv).init({ mode: FactoryMode.Fetch, writeMode: WriteMode.WriteIfFree, id });
|
||||
}
|
||||
getAvailableSubroomId() {
|
||||
async getAllSubrooms() {
|
||||
return new Set(await Promise.all(
|
||||
this.getSubroomIds().values().map(subroom => this.getSubroom(subroom))
|
||||
));
|
||||
}
|
||||
async getAvailableSubroomId() {
|
||||
let id = Math.round(Math.random() * Math.pow(2, 31));
|
||||
if (this.getSubrooms().has(id)) id = this.getAvailableSubroomId();
|
||||
if ((await this.#subroomKv.getKv().get<unknown>([id])).value) id = await 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() });
|
||||
return await new SubroomFactory(this.#server, this.#subroomKv).init({ mode: FactoryMode.Write, writeMode: mode ? mode : WriteMode.WriteIfFree, id: await this.getAvailableSubroomId() });
|
||||
}
|
||||
addSubroom(id: number) {
|
||||
if (!this.#subrooms) throw this.#cannotAccessBeforeInitError;
|
||||
|
||||
@@ -2,6 +2,7 @@ import Logging from "@proxnet/undead-logging";
|
||||
import type KV from "../../persistence/kv.ts";
|
||||
import { type ServerBase } from "../../server.ts";
|
||||
import { DatabaseSubroom, FactoryMode, RoomDataTypes, RoomSave, RoomSaveMap, WriteMode } from "./RoomDataTypes.ts";
|
||||
import { RoomLocation } from "../../instances/types.ts";
|
||||
|
||||
export interface SubroomFactoryOptions {
|
||||
mode: FactoryMode,
|
||||
@@ -44,7 +45,7 @@ export class SubroomFactory {
|
||||
|
||||
this.#obj = options.mode == FactoryMode.Fetch ? data.value : {
|
||||
RoomId: 0,
|
||||
RoomSceneLocationId: "",
|
||||
RoomSceneLocationId: RoomLocation.MakerRoom,
|
||||
Name: "Subroom data init failed, contact an admin!",
|
||||
IsSandbox: false,
|
||||
MaxPlayers: 8,
|
||||
@@ -92,7 +93,7 @@ export class SubroomFactory {
|
||||
|
||||
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; }
|
||||
get RoomSceneLocationId(): RoomLocation { 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; }
|
||||
|
||||
@@ -3,11 +3,14 @@ import { ServerUpdateEvent } from "../serverevents.ts";
|
||||
import { AvatarContentBase } from "./avatars/base.ts";
|
||||
import { EventManager } from "./baseevent.ts";
|
||||
import { CommandsBase } from "./commands/commands.ts";
|
||||
import { ServerConsumablesBase } from "./consumables/base.ts";
|
||||
import { ServerContentManager } from "./content/base.ts";
|
||||
import GameConfigsBase from "./gameconfigs/base.ts";
|
||||
import { InstanceManager } from "./instances/base.ts";
|
||||
import { ServerMatchmakingBase } from "./matchmaking/base.ts";
|
||||
import { Objective, ObjectiveType } from "./objectives/base.ts";
|
||||
import { PlatformsManager } from "./platforms/base.ts";
|
||||
import { ServerPresenceBase } from "./presence/base.ts";
|
||||
import { type PresenceUpdateEvent } from "./presence/events/PresenceUpdateEvent.ts";
|
||||
import { type ProfileUpdateEvent } from "./profiles/events/ProfileUpdate.ts";
|
||||
import { ProfileUpdatedSettingEvent } from "./profiles/events/ProfileUpdatedSetting.ts";
|
||||
@@ -15,6 +18,7 @@ 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/RoomEvents.ts";
|
||||
import { AnnouncementDTO } from "./types.ts";
|
||||
|
||||
interface ServerEvents {
|
||||
'profile.roleupdate': RoleUpdateEvent,
|
||||
@@ -42,7 +46,7 @@ interface AutoMicMutingConfig {
|
||||
MicSpamWarningStateVolumeMultiplier: number;
|
||||
};
|
||||
|
||||
export type PublicConfig = {
|
||||
export interface PublicConfig {
|
||||
ShareBaseUrl: string;
|
||||
ServerMaintenance: {
|
||||
StartsInMinutes: number;
|
||||
@@ -61,6 +65,9 @@ class ServerBase extends EventManager<ServerEvents> {
|
||||
Instances = new InstanceManager(this, 'instances');
|
||||
Content = new ServerContentManager(this, "content");
|
||||
Rooms = new ServerRoomsBase(this, 'rooms', true);
|
||||
Matchmaking = new ServerMatchmakingBase(this, "match");
|
||||
Presence = new ServerPresenceBase(this, "pres");
|
||||
Consumables = new ServerConsumablesBase(this, "consumables");
|
||||
|
||||
generateMask(...num: number[]) {
|
||||
return num.reduce((sum, val) => sum + val, 0);
|
||||
@@ -137,6 +144,11 @@ class ServerBase extends EventManager<ServerEvents> {
|
||||
|
||||
return conf;
|
||||
}
|
||||
|
||||
getAnnouncements(): AnnouncementDTO[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const Server = new ServerBase();
|
||||
|
||||
@@ -4,7 +4,8 @@ import { type ConsoleItem, ConsoleItemSchema } from "./zod.ts";
|
||||
import Server from "../../server.ts";
|
||||
import { getSourceAddress } from "../../../util/net.ts";
|
||||
import { consoleSockets } from "../../../main.ts";
|
||||
import chalk from "npm:chalk@^5.3.0";
|
||||
import chalk from "chalk";
|
||||
import { CommandSenderType } from "../../commands/cmdtypes.ts";
|
||||
|
||||
export default class SocketConsoleHandler {
|
||||
|
||||
@@ -55,7 +56,7 @@ export default class SocketConsoleHandler {
|
||||
if (!zodParsed.success) this.destroy();
|
||||
|
||||
else if (zodParsed.data.e == ConsoleEvent.Command) {
|
||||
const data = await Server.Commands.dispatch(...zodParsed.data.d.split(' '));
|
||||
const data = await Server.Commands.dispatch({ type: CommandSenderType.Console }, ...zodParsed.data.d.split(' '));
|
||||
if (data instanceof Error) throw data;
|
||||
|
||||
this.send(ConsoleEvent.Message, chalk.gray(`> ${chalk.yellow(data)}`));
|
||||
|
||||
@@ -18,6 +18,8 @@ import { SocketTarget } from "./targets/targetbase.ts";
|
||||
import type Profile from "../../profiles/profile.ts";
|
||||
import { detailedLog } from "../../../main.ts";
|
||||
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
||||
import Server from "../../server.ts";
|
||||
import { PresenceUpdateEvent } from "../../presence/events/PresenceUpdateEvent.ts";
|
||||
|
||||
const logmessages = true;
|
||||
|
||||
@@ -32,6 +34,10 @@ export class SignalRSocketHandler {
|
||||
|
||||
#killed = false;
|
||||
|
||||
#presCb = (ev: PresenceUpdateEvent) => {
|
||||
if (ev.profile == this.#profile) this.sendNotification("PresenceUpdate", ev.presence.export());
|
||||
}
|
||||
|
||||
constructor(socket: WebSocket, player: Profile) {
|
||||
|
||||
this.#socket = socket;
|
||||
@@ -69,6 +75,8 @@ export class SignalRSocketHandler {
|
||||
}
|
||||
|
||||
async #onMessage(message: Message) {
|
||||
this.#profile.Matchmaking.updateLastSeen();
|
||||
|
||||
if (message.kind == MessageKind.Protocol) {
|
||||
this.sendRaw({});
|
||||
return;
|
||||
@@ -130,6 +138,8 @@ export class SignalRSocketHandler {
|
||||
}
|
||||
});
|
||||
|
||||
Server.on('presence.update', this.#presCb);
|
||||
|
||||
this.#socket.addEventListener('close', this.destroy(this, true));
|
||||
}
|
||||
|
||||
@@ -137,10 +147,12 @@ export class SignalRSocketHandler {
|
||||
return (ev: CloseEvent) => {
|
||||
handler.#killed = true;
|
||||
|
||||
Server.off('presence.update', this.#presCb);
|
||||
|
||||
let errorReason = "Socket closed by server";
|
||||
this.#log.d(`Socket close code: ${ev.code}`);
|
||||
if (ev.reason.includes('Bye!')) errorReason = "Socket closed by client request";
|
||||
|
||||
|
||||
handler.sendRaw({ type: 7, error: errorReason });
|
||||
|
||||
if (!internal) handler.#socket.close();
|
||||
|
||||
@@ -6,7 +6,7 @@ export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
||||
#ids: number[] = [];
|
||||
|
||||
override zod = z.object({
|
||||
PlayerIds: z.array(z.number().nonnegative().max(2_147_483_647))
|
||||
PlayerIds: z.array(z.number().nonnegative().max(Math.pow(2, 31)))
|
||||
});
|
||||
|
||||
override exec(...ids: number[]) {
|
||||
|
||||
29
src/server/types.ts
Normal file
29
src/server/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { PlatformType } from "./platforms/types.ts";
|
||||
|
||||
export enum AnnouncementType {
|
||||
Update,
|
||||
Contest,
|
||||
Store,
|
||||
Event,
|
||||
Warning
|
||||
}
|
||||
enum AnnouncementLinkType {
|
||||
Url,
|
||||
AccountId,
|
||||
EventId,
|
||||
RoomName,
|
||||
Storefront
|
||||
}
|
||||
|
||||
export interface AnnouncementDTO {
|
||||
AnnouncementId: number,
|
||||
AnnouncementType: AnnouncementType,
|
||||
Title: string,
|
||||
Body: string,
|
||||
ImageName: string,
|
||||
LinkType: AnnouncementLinkType,
|
||||
LinkName: string,
|
||||
LinkUri: string,
|
||||
Platform: PlatformType,
|
||||
CreatedAt: string,
|
||||
}
|
||||
Reference in New Issue
Block a user