forked from zombieb/galvanic-corrosion-rewrite
galvanic corrosion rewrite
commit this before something goes horribly wrong
This commit is contained in:
@@ -15,7 +15,7 @@ export class ServerContentBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Event method - ran when server starts (listens on address)
|
||||
* Event method - ran before server starts
|
||||
*
|
||||
* Override me!
|
||||
*/
|
||||
@@ -23,4 +23,13 @@ export class ServerContentBase {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event method - ran before server stops
|
||||
*
|
||||
* Override me!
|
||||
*/
|
||||
destroy() {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from "node:path";
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import { RootPath } from "../../util/path.ts";
|
||||
import { PlatformMask } from "../platforms/types.ts";
|
||||
|
||||
interface AvatarImport {
|
||||
allPossibleCombinations: {
|
||||
@@ -20,6 +21,7 @@ interface AvatarItemExport {
|
||||
AvatarItemType: AvatarItemType,
|
||||
AvatarItemDesc: string,
|
||||
FriendlyName: string,
|
||||
PlatformMask: number,
|
||||
Tooltip: string,
|
||||
Rarity: ItemRarity
|
||||
}
|
||||
@@ -61,7 +63,8 @@ export class AvatarContentBase extends ServerContentBase {
|
||||
AvatarItemType: AvatarItemType.Outfit,
|
||||
AvatarItemDesc: formatVisualData(data._avatarItemVisualData),
|
||||
FriendlyName: data._avatarItemData.Name,
|
||||
Tooltip: "pre-avatar update item",
|
||||
PlatformMask: PlatformMask.All,
|
||||
Tooltip: "Galvanic Avatar Item",
|
||||
Rarity: ItemRarity.None
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
enum _OutfitType {
|
||||
None = -1,
|
||||
Hat,
|
||||
Hair = 2,
|
||||
Ear,
|
||||
Eye = 10,
|
||||
Beard = 20,
|
||||
Shoulder = 100,
|
||||
Shirt,
|
||||
Waist,
|
||||
Neck,
|
||||
TeamJersey,
|
||||
Wrist = 200,
|
||||
TeamWrist = 203
|
||||
}
|
||||
|
||||
// figure out the order in which ids go into AvatarItemDesc
|
||||
// then create a function in `base.ts` (next to this file) that can turn enums corresponding to avatar items into a full AvatarItemDesc string
|
||||
// - that may require codegen. i can probably do it though. i know you can.
|
||||
181
src/server/content/base.ts
Normal file
181
src/server/content/base.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import type Profile from "../profiles/profile.ts";
|
||||
import { generateRandomString } from "../../util/api.ts";
|
||||
|
||||
export enum FileType {
|
||||
Unknown,
|
||||
RoomSave,
|
||||
Holotar,
|
||||
Image,
|
||||
Video,
|
||||
Invention
|
||||
}
|
||||
interface RawMetaFile {
|
||||
Type: FileType,
|
||||
CreatedAt: string,
|
||||
SavedBy?: number
|
||||
OriginalFilename?: string
|
||||
}
|
||||
interface MetaFile {
|
||||
Type: FileType,
|
||||
CreatedAt: Date,
|
||||
SavedBy?: Profile
|
||||
OriginalFilename?: string
|
||||
}
|
||||
interface File {
|
||||
Meta: MetaFile,
|
||||
Data: Uint8Array<ArrayBufferLike>
|
||||
}
|
||||
|
||||
interface FileCreationResult {
|
||||
success: boolean
|
||||
}
|
||||
interface FileCreationSuccess extends FileCreationResult {
|
||||
success: true,
|
||||
newFilename: string
|
||||
}
|
||||
interface FileCreationFailure extends FileCreationResult {
|
||||
success: false,
|
||||
error: Error
|
||||
}
|
||||
type FileCreation = FileCreationSuccess | FileCreationFailure;
|
||||
|
||||
export class ServerContentManager extends ServerContentBase {
|
||||
|
||||
#log = new Logging("ServerContent");
|
||||
|
||||
override start() {
|
||||
Array.fromAsync(Deno.readDir('./persist')).then(entries => {
|
||||
if (!entries.find(entry => entry.isDirectory && entry.name == 'user')) {
|
||||
this.#log.i(`Creating user folders`);
|
||||
this.#createUserFolders();
|
||||
}
|
||||
});
|
||||
}
|
||||
async #createUserFolders() {
|
||||
await Deno.mkdir('./persist/user');
|
||||
await Deno.mkdir('./persist/user/room');
|
||||
await Deno.mkdir('./persist/user/holotar');
|
||||
await Deno.mkdir('./persist/user/img');
|
||||
await Deno.mkdir('./persist/user/video');
|
||||
await Deno.mkdir('./persist/user/invention');
|
||||
}
|
||||
|
||||
async steamAvatarDownloadForProfile(prof: Profile, steamUrl: string) {
|
||||
await fetch(steamUrl).then(async res => {
|
||||
const url = new URL(res.url);
|
||||
const split = url.pathname.split('/');
|
||||
const filename = split[split.length - 1];
|
||||
|
||||
this.saveFile(await res.bytes(), FileType.Image, filename).then(res => {
|
||||
if (res.success == true) {
|
||||
prof.setProfileImg(res.newFilename);
|
||||
this.#log.i(`Saved profile image from Steam for profile ${prof.getId()}: "${res.newFilename}"`);
|
||||
}
|
||||
else this.#log.w(`Could not save profile image from Steam for profile ${prof.getId()}: ${res.error}`);
|
||||
});
|
||||
}).catch(reas => {
|
||||
this.#log.w(`Could not fetch steam URL and download for profile: ${reas}`);
|
||||
});
|
||||
}
|
||||
|
||||
async getAvailabileFileName(ext: string, prefix: string) {
|
||||
let filename = generateRandomString(18);
|
||||
while ((await Array.fromAsync(Deno.readDir(`./persist/user/${prefix ? prefix : ""}`)))
|
||||
.find(entry => entry.isFile && entry.name == `${filename}.${ext}`)) filename = await this.getAvailabileFileName(ext, prefix);
|
||||
return filename;
|
||||
}
|
||||
|
||||
fileTypeToExt(type: FileType) {
|
||||
switch (type) {
|
||||
case FileType.RoomSave:
|
||||
return "room";
|
||||
case FileType.Holotar:
|
||||
return "holo";
|
||||
case FileType.Image:
|
||||
return "img";
|
||||
case FileType.Video:
|
||||
return "vid";
|
||||
case FileType.Invention:
|
||||
return "inv";
|
||||
default:
|
||||
return "blob";
|
||||
}
|
||||
}
|
||||
|
||||
async saveFile(data: Uint8Array<ArrayBufferLike>, type: FileType, filename?: string, prof?: Profile): Promise<FileCreation> {
|
||||
|
||||
let targetFolder = "";
|
||||
switch (type) {
|
||||
case FileType.RoomSave:
|
||||
targetFolder = "room/";
|
||||
break;
|
||||
case FileType.Holotar:
|
||||
targetFolder = "holotar/";
|
||||
break;
|
||||
case FileType.Image:
|
||||
targetFolder = "img/";
|
||||
break;
|
||||
case FileType.Video:
|
||||
targetFolder = "video/";
|
||||
break;
|
||||
case FileType.Invention:
|
||||
targetFolder = "invention/";
|
||||
break;
|
||||
}
|
||||
const ext = this.fileTypeToExt(type);
|
||||
const newFilename = await this.getAvailabileFileName(ext, targetFolder);
|
||||
|
||||
try {
|
||||
await Deno.writeFile(`./persist/user/${targetFolder}${newFilename}.${ext}`, data);
|
||||
|
||||
const metaRaw: RawMetaFile = {
|
||||
Type: type,
|
||||
CreatedAt: new Date().toISOString(),
|
||||
SavedBy: prof?.getId(),
|
||||
OriginalFilename: filename
|
||||
}
|
||||
await Deno.writeTextFile(`./persist/user/${targetFolder}${newFilename}.${ext}.meta`, JSON.stringify(metaRaw));
|
||||
|
||||
const success: FileCreationSuccess = {
|
||||
success: true,
|
||||
newFilename: `${newFilename}.${ext}`
|
||||
}
|
||||
return success;
|
||||
} catch (err) {
|
||||
this.#log.w(`Could not save file (typ: ${FileType}, name: ${filename}, prof: ${prof ? prof.getId() : undefined}): ${err}`);
|
||||
|
||||
const error: FileCreationFailure = {
|
||||
success: false,
|
||||
error: (err as Error)
|
||||
}
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
async getFile(path: string) {
|
||||
try {
|
||||
const data = await Deno.readFile(`./persist/user/${path}`);
|
||||
const meta = await Deno.readTextFile(`./persist/user/${path}.meta`);
|
||||
|
||||
const metaRaw: RawMetaFile = JSON.parse(meta);
|
||||
const metaParsed: MetaFile = {
|
||||
Type: metaRaw.Type,
|
||||
CreatedAt: new Date(metaRaw.CreatedAt),
|
||||
SavedBy: metaRaw.SavedBy ? (await this.server.Profiles.get(metaRaw.SavedBy) ?? undefined) : undefined,
|
||||
OriginalFilename: metaRaw.OriginalFilename
|
||||
}
|
||||
|
||||
const file: File = {
|
||||
Meta: metaParsed,
|
||||
Data: data
|
||||
}
|
||||
return file;
|
||||
} catch (err) {
|
||||
this.#log.w(`Could not get file "${path}": ${err}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
39
src/server/instances/Instance.ts
Normal file
39
src/server/instances/Instance.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type Profile from "../profiles/profile.ts";
|
||||
import { RoomLocation } from "./base.ts";
|
||||
|
||||
export class Instance {
|
||||
|
||||
#createdAt = new Date();
|
||||
|
||||
#players: Set<Profile> = new Set();
|
||||
|
||||
#instanceId: number;
|
||||
#location: RoomLocation;
|
||||
|
||||
constructor(options: {
|
||||
id: number,
|
||||
location: RoomLocation,
|
||||
}
|
||||
) {
|
||||
this.#instanceId = options.id;
|
||||
this.#location = options.location;
|
||||
}
|
||||
|
||||
getPlayers() {
|
||||
return this.#players.values().toArray();
|
||||
}
|
||||
playerIsHere(profile: Profile) {
|
||||
return this.getPlayers().find(prof => prof.same(profile)) ? true : false;
|
||||
}
|
||||
removePlayer(profile: Profile) {
|
||||
this.#players.delete(profile);
|
||||
}
|
||||
addPlayer(profile: Profile) {
|
||||
this.#players.add(profile);
|
||||
}
|
||||
|
||||
getCreatedAt() {
|
||||
return this.#createdAt;
|
||||
}
|
||||
|
||||
}
|
||||
63
src/server/instances/base.ts
Normal file
63
src/server/instances/base.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
|
||||
export enum RoomLocation {
|
||||
Calibration = "f5fbd9c9-e853-4036-9d48-5f68e861af04",
|
||||
DormRoom = "76d98498-60a1-430c-ab76-b54a29b7a163",
|
||||
RecCenter = "cbad71af-0831-44d8-b8ef-69edafa841f6",
|
||||
Charades = "4078dfed-24bb-4db7-863f-578ba48d726b",
|
||||
TheInkSpace = "1fa06e3c-c307-4c11-a91b-1fabcddb8a96",
|
||||
Paddleball = "d89f74fa-d51e-477a-a425-025a891dd499",
|
||||
GoldenTrophy = "91e16e35-f48f-4700-ab8a-a1b79e50e51b",
|
||||
Orientation = "c79709d8-a31b-48aa-9eb8-cc31ba9505e8",
|
||||
TheRiseofJumbotron = "acc06e66-c2d0-4361-b0cd-46246a4c455c",
|
||||
CrimsonCauldron = "949fa41f-4347-45c0-b7ac-489129174045",
|
||||
IsleOfLostSkulls = "7e01cfe0-820a-406f-b1b3-0a5bf575235c",
|
||||
RecRoyaleSquads = "253fa009-6e65-4c90-91a1-7137a56a267f",
|
||||
RecRoyaleSolos = "b010171f-4875-4e89-baba-61e878cd41e1",
|
||||
Lounge = "a067557f-ca32-43e6-b6e5-daaec60b4f5a",
|
||||
PerformanceHall = "9932f88f-3929-43a0-a012-a40b5128e346",
|
||||
MakerRoom = "a75f7547-79eb-47c6-8986-6767abcb4f92",
|
||||
Park = "0a864c86-5a71-4e18-8041-8124e4dc9d98",
|
||||
Lake = "f6f7256c-e438-4299-b99e-d20bef8cf7e0",
|
||||
PropulsionTestRange = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55",
|
||||
Gym = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
|
||||
Stadium = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03",
|
||||
Hangar = "239e676c-f12f-489f-bf3a-d4c383d692c3",
|
||||
CyberJunkCity = "9d6456ce-6264-48b4-808d-2d96b3d91038",
|
||||
Crescendo = "49cb8993-a956-43e2-86f4-1318f279b22a",
|
||||
BowlingAlley = "ae929543-9a07-41d5-8ee9-dbbee8c36800",
|
||||
AnimationRecordingStudio = "a95c349c-0f96-4c2d-a4c8-4969ffa8ea44",
|
||||
StuntRunner = "b7281665-a715-4051-826b-8e08e69c6172",
|
||||
TheMainEvent = "3a636bd2-f896-424c-9225-c184522c0d87",
|
||||
StuntRunnerBaseRoom = "882e9b96-7115-4b03-86f6-c0c9d8e22e00",
|
||||
Registration = "cf61556d-68fd-4288-9ae5-7a512621e569",
|
||||
ARRoom = "bf268f5f-b55b-41af-8628-32fa4b5d70b6",
|
||||
PaintballRiver = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
|
||||
PaintballHomestead = "a785267d-c579-42ea-be43-fec1992d1ca7",
|
||||
PaintballQuarry = "ff4c6427-7079-4f59-b22a-69b089420827",
|
||||
PaintballClearcut = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
|
||||
PaintballSpillway = "58763055-2dfb-4814-80b8-16fac5c85709",
|
||||
PaintballDriveIn = "65ddbb48-5a01-4e3e-972d-e5c7419e2bc3",
|
||||
}
|
||||
|
||||
export interface RoomInstance {
|
||||
roomInstanceId: number,
|
||||
roomId: number,
|
||||
subRoomId: number,
|
||||
location: RoomLocation,
|
||||
name: string,
|
||||
maxCapacity: number,
|
||||
isFull: boolean,
|
||||
isPrivate: boolean,
|
||||
isInProgress: boolean,
|
||||
photonRegionId: string,
|
||||
photonRoomId: string,
|
||||
dataBlob?: string,
|
||||
eventId?: string
|
||||
}
|
||||
|
||||
export class InstanceManager extends ServerContentBase {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -3,56 +3,13 @@ import Command from "../commands/command.ts";
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import { transformStringToEnum } from "../../util/validators.ts";
|
||||
import { sign } from "@hono/hono/jwt";
|
||||
|
||||
export enum PlatformType {
|
||||
All = -1,
|
||||
Steam,
|
||||
Oculus,
|
||||
PlayStation,
|
||||
Xbox,
|
||||
WindowsPlatformless,
|
||||
IOS,
|
||||
GooglePlay
|
||||
}
|
||||
|
||||
export enum DeviceClass {
|
||||
Unknown,
|
||||
VR,
|
||||
Screen,
|
||||
Mobile,
|
||||
VRLow,
|
||||
Quest2
|
||||
}
|
||||
|
||||
|
||||
interface DbCachedLogin {
|
||||
accountId: number,
|
||||
lastLoginTime: Date,
|
||||
requirePassword: boolean
|
||||
}
|
||||
export interface CachedLogin extends DbCachedLogin {
|
||||
platformId: string
|
||||
platform: PlatformType
|
||||
}
|
||||
import { CachedLogin, DbCachedLogin, PlatformMask, PlatformType, TokenFormat, TokenType } from "./types.ts";
|
||||
|
||||
export const steamAuthTicketSchema = z.object({
|
||||
Ticket: z.string().min(256),
|
||||
AppId: z.literal("471710")
|
||||
});
|
||||
|
||||
export enum ProfileRole {
|
||||
Developer = "developer",
|
||||
Moderator = "moderator",
|
||||
Screenshare = "screenshare",
|
||||
User = "user"
|
||||
}
|
||||
interface TokenFormat {
|
||||
iss: string,
|
||||
exp: number,
|
||||
sub: number,
|
||||
role: string
|
||||
}
|
||||
|
||||
export class PlatformsManager extends ServerContentBase {
|
||||
|
||||
static platformsKey = "platforms";
|
||||
@@ -61,15 +18,15 @@ export class PlatformsManager extends ServerContentBase {
|
||||
return [PlatformsManager.platformsKey, ...keys.filter(val => typeof val == 'string')];
|
||||
}
|
||||
|
||||
async getToken(accountId: number, role: string) {
|
||||
async getToken(accountId: number, type: TokenType) {
|
||||
const secret = Deno.env.get('SECRET');
|
||||
if (!secret) throw new Error("No SECRET in env. Did you forget to set it?");
|
||||
|
||||
const token: TokenFormat = {
|
||||
typ: type,
|
||||
sub: accountId,
|
||||
role,
|
||||
iss: "https://wsi.proxnet.dev/auth/",
|
||||
exp: Math.round(Date.now() / 1000) + 21_600
|
||||
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
|
||||
}
|
||||
return await sign(JSON.parse(JSON.stringify(token)), secret);
|
||||
}
|
||||
@@ -106,8 +63,8 @@ export class PlatformsManager extends ServerContentBase {
|
||||
};
|
||||
}
|
||||
|
||||
async getCachedLogins(platform: PlatformType, platformId: string, format: true): Promise<CachedLogin[] | null>
|
||||
async getCachedLogins(platform: PlatformType, platformId: string, format: false): Promise<DbCachedLogin[] | null>
|
||||
async getCachedLogins(platform: PlatformType, platformId: string, format: true): Promise<CachedLogin[]>
|
||||
async getCachedLogins(platform: PlatformType, platformId: string, format: false): Promise<DbCachedLogin[]>
|
||||
async getCachedLogins(platform: PlatformType, platformId: string, format?: boolean) {
|
||||
const set = await this.kv.getKv().get<Set<DbCachedLogin>>(this.#constructPlatformKey(platform, platformId));
|
||||
if (set.value && format) return set.value.values().toArray().map(val => ({
|
||||
@@ -118,7 +75,7 @@ export class PlatformsManager extends ServerContentBase {
|
||||
requirePassword: val.requirePassword
|
||||
} as CachedLogin));
|
||||
else if (set.value) return set.value.values().toArray();
|
||||
else return null;
|
||||
else return [];
|
||||
}
|
||||
|
||||
async deleteCachedLogin(platform: PlatformType, platformId: string, accountId: number) {
|
||||
@@ -136,6 +93,30 @@ export class PlatformsManager extends ServerContentBase {
|
||||
} else return null;
|
||||
}
|
||||
|
||||
getPlatformMask(value: number) {
|
||||
const err = new Error("Invalid mask");
|
||||
if (typeof value !== 'number' || !Number.isInteger(value)) throw err;
|
||||
|
||||
if (value === PlatformMask.All) {
|
||||
return [PlatformMask.All];
|
||||
}
|
||||
|
||||
return Object.values(PlatformMask)
|
||||
.filter(v => typeof v === "number" && v !== PlatformMask.None && v !== PlatformMask.All)
|
||||
.filter(v => (value & (v as number)) === v) as PlatformMask[];
|
||||
}
|
||||
|
||||
buildPlatformMask(...flags: PlatformMask[]) {
|
||||
const err = new Error("Invalid mask");
|
||||
if (!flags.length) throw err;
|
||||
|
||||
for (const flag of flags)
|
||||
if (typeof flag !== 'number' || !Object.values(PlatformMask).includes(flag))
|
||||
throw err;
|
||||
|
||||
return flags.reduce((mask, flag) => mask | flag, 0);
|
||||
}
|
||||
|
||||
override start() {
|
||||
this.server.Commands.addRootCommand(new Command({
|
||||
key: ['platforms', 'pm', 'platformmanager', 'platformanager'],
|
||||
|
||||
59
src/server/platforms/types.ts
Normal file
59
src/server/platforms/types.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
export enum TokenType {
|
||||
Access,
|
||||
Refresh
|
||||
}
|
||||
export interface TokenFormatBase {
|
||||
typ: TokenType
|
||||
}
|
||||
export interface TokenFormat extends TokenFormatBase {
|
||||
iss: string,
|
||||
exp: number,
|
||||
sub: number,
|
||||
}
|
||||
|
||||
export enum ProfileRole {
|
||||
Developer = 'developer',
|
||||
Moderator = 'moderator',
|
||||
Web = 'webClient',
|
||||
Game = 'gameClient'
|
||||
}
|
||||
|
||||
export enum PlatformType {
|
||||
All = -1,
|
||||
Steam,
|
||||
Oculus,
|
||||
PlayStation,
|
||||
Microsoft,
|
||||
HeadlessBot,
|
||||
IOS,
|
||||
}
|
||||
|
||||
export enum PlatformMask {
|
||||
None = 0,
|
||||
Steam = 1,
|
||||
Oculus = 2,
|
||||
PlayStation = 4,
|
||||
Microsoft = 8,
|
||||
HeadlessBot = 16,
|
||||
IOS = 32,
|
||||
All = -1
|
||||
}
|
||||
|
||||
export enum DeviceClass {
|
||||
Unknown,
|
||||
VR,
|
||||
Screen,
|
||||
Mobile,
|
||||
VRLow
|
||||
}
|
||||
|
||||
export interface DbCachedLogin {
|
||||
accountId: number,
|
||||
lastLoginTime: Date,
|
||||
requirePassword: boolean
|
||||
}
|
||||
|
||||
export interface CachedLogin extends DbCachedLogin {
|
||||
platformId: string
|
||||
platform: PlatformType
|
||||
}
|
||||
@@ -1,16 +1,132 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { ServerContentBase } from "../ContentBase.ts";
|
||||
import type Profile from "../profiles/profile.ts";
|
||||
import { DeviceClass } from "../platforms/types.ts";
|
||||
import Profile from "../profiles/profile.ts";
|
||||
import { type ServerBase } from "../server.ts";
|
||||
import { RoomInstance } from "../instances/base.ts";
|
||||
|
||||
class Presence {
|
||||
export enum VRMovementMode {
|
||||
TELEPORT,
|
||||
WALK
|
||||
}
|
||||
|
||||
export enum PlayerStatusVisibility {
|
||||
Public,
|
||||
FriendsOnly,
|
||||
FavoriteFriendsOnly,
|
||||
Offline
|
||||
}
|
||||
|
||||
export interface PresenceExport {
|
||||
playerId: number,
|
||||
statusVisibility: PlayerStatusVisibility,
|
||||
deviceClass: DeviceClass,
|
||||
vrMovementMode?: VRMovementMode,
|
||||
roomInstance: RoomInstance | null
|
||||
}
|
||||
|
||||
export class Presence {
|
||||
|
||||
#server: ServerBase;
|
||||
|
||||
#profile: Profile;
|
||||
|
||||
#statusVisibility: PlayerStatusVisibility = PlayerStatusVisibility.Offline;
|
||||
#deviceClass: DeviceClass = DeviceClass.Unknown;
|
||||
#roomInstance: RoomInstance | null = null;
|
||||
#vrMovementMove: VRMovementMode | undefined;
|
||||
|
||||
#lastExported: Date = new Date();
|
||||
|
||||
constructor(profile: Profile, server: ServerBase) {
|
||||
this.#profile = profile;
|
||||
this.#server = server;
|
||||
}
|
||||
|
||||
/** Refer to `Profile.Matchmaking.updateLastSeen` */
|
||||
updateLastSeen() {
|
||||
this.#profile.Matchmaking.updateLastSeen();
|
||||
}
|
||||
|
||||
async update() {
|
||||
this.#deviceClass = (await this.#profile.Matchmaking.getLastDeviceClass()) || DeviceClass.Unknown;
|
||||
const isOnline = (Date.now() - (await this.#profile.Matchmaking.getLastSeen()).getTime()) < 90_000;
|
||||
this.#statusVisibility =
|
||||
isOnline ?
|
||||
this.#statusVisibility :
|
||||
PlayerStatusVisibility.Offline;
|
||||
this.#roomInstance = this.#profile.getInstance();
|
||||
|
||||
this.#server.emit('presence.update', { profile: this.#profile, presence: this });
|
||||
}
|
||||
|
||||
setStatusVisibility(sv: PlayerStatusVisibility) {
|
||||
this.#statusVisibility = sv;
|
||||
this.updateLastSeen();
|
||||
this.update();
|
||||
}
|
||||
|
||||
setVRMovementMode(mm: VRMovementMode) {
|
||||
this.#vrMovementMove = mm;
|
||||
this.updateLastSeen();
|
||||
this.update();
|
||||
}
|
||||
|
||||
getLastExported() {
|
||||
return this.#lastExported;
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.updateLastSeen();
|
||||
this.#statusVisibility = PlayerStatusVisibility.Offline;
|
||||
}
|
||||
|
||||
export() {
|
||||
this.#lastExported = new Date();
|
||||
const e: PresenceExport = {
|
||||
playerId: this.#profile.getId(),
|
||||
statusVisibility: this.#statusVisibility,
|
||||
deviceClass: this.#deviceClass,
|
||||
vrMovementMode: this.#vrMovementMove,
|
||||
roomInstance: this.#roomInstance
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PresenceBase extends ServerContentBase {
|
||||
|
||||
#log = new Logging("Presence");
|
||||
|
||||
#presenceMap: Map<Profile, Presence> = new Map();
|
||||
|
||||
getPresence() {
|
||||
|
||||
#intervalId?: number;
|
||||
|
||||
#deleteDeadPresences() {
|
||||
for (const pres of this.#presenceMap.values()) {
|
||||
if (Date.now() - pres.getLastExported().getTime() > 300_000) pres
|
||||
}
|
||||
}
|
||||
|
||||
getPresence(profile: Profile) {
|
||||
let pres = this.#presenceMap.get(profile);
|
||||
if (!pres) {
|
||||
pres = new Presence(profile, this.server);
|
||||
this.#presenceMap.set(profile, pres);
|
||||
}
|
||||
return pres;
|
||||
}
|
||||
|
||||
override start() {
|
||||
this.#intervalId = setInterval(() => {
|
||||
this.#log.i('Clearing dead presences');
|
||||
this.#deleteDeadPresences();
|
||||
}, 300_000);
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
clearInterval(this.#intervalId ?? undefined);
|
||||
}
|
||||
|
||||
}
|
||||
7
src/server/presence/events/PresenceUpdateEvent.ts
Normal file
7
src/server/presence/events/PresenceUpdateEvent.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type Profile from "../../profiles/profile.ts";
|
||||
import { type Presence } from "../base.ts";
|
||||
|
||||
export interface PresenceUpdateEvent {
|
||||
presence: Presence,
|
||||
profile: Profile
|
||||
}
|
||||
@@ -1,23 +1,14 @@
|
||||
import { DeviceClass } from "../../platforms/types.ts";
|
||||
import ProfileContentManager from "./base.ts";
|
||||
|
||||
export enum DeviceClass {
|
||||
Unknown,
|
||||
VR,
|
||||
Screen,
|
||||
Mobile,
|
||||
VRLow,
|
||||
Quest2
|
||||
}
|
||||
|
||||
|
||||
export class ProfileMatchmakingManager extends ProfileContentManager {
|
||||
|
||||
#deviceClassKey = this.profile.constructProfilePropertyKey('deviceclass');
|
||||
async setLastDeviceClass(dc: DeviceClass) {
|
||||
await this.kv.getKv().set(this.#deviceClassKey, dc);
|
||||
}
|
||||
async getLastDeviceClass(): Promise<DeviceClass | null> {
|
||||
return (await this.kv.getKv().get<DeviceClass>(this.#deviceClassKey)).value || null;
|
||||
async getLastDeviceClass(): Promise<DeviceClass> {
|
||||
return (await this.kv.getKv().get<DeviceClass>(this.#deviceClassKey)).value || DeviceClass.Unknown;
|
||||
}
|
||||
|
||||
#loginLockKey = this.profile.constructProfilePropertyKey('loginlock');
|
||||
@@ -31,4 +22,14 @@ export class ProfileMatchmakingManager extends ProfileContentManager {
|
||||
return (await this.kv.getKv().get<string>(this.#loginLockKey)).value ? true : false;
|
||||
}
|
||||
|
||||
#lastSeen = this.profile.constructProfilePropertyKey('lastseen');
|
||||
async updateLastSeen() {
|
||||
await this.kv.getKv().set(this.#lastSeen, new Date());
|
||||
}
|
||||
async getLastSeen() {
|
||||
const value = await this.kv.getKv().get<Date>(this.#lastSeen);
|
||||
if (value.value) return value.value;
|
||||
else return this.profile.getCreationDate();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ class ProfileContentManager {
|
||||
constructor(profile: Profile, kv: KV) {
|
||||
this.profile = profile;
|
||||
this.kv = kv;
|
||||
profile.managers.push(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ProfileRole } from "../../platforms/base.ts";
|
||||
import { type ProfileRole } from "../../platforms/types.ts";
|
||||
import type Profile from "../profile.ts";
|
||||
|
||||
export interface RoleUpdateEvent {
|
||||
|
||||
@@ -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 { ProfileRole } from "../platforms/base.ts";
|
||||
import { PlatformMask, PlatformType, ProfileRole } from "../platforms/types.ts";
|
||||
|
||||
const profiles: Map<number, Profile> = new Map();
|
||||
|
||||
@@ -13,15 +13,15 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
static profilesKey = "profiles";
|
||||
static profileByNameKey = "profileName";
|
||||
|
||||
/*async exists(id: number) {
|
||||
return (await this.kv.getKv().get([ ProfileManagerBase.profilesKey, id ])).value !== null
|
||||
}*/
|
||||
|
||||
async #getUnusedId() {
|
||||
let id = Math.round(Math.random() * 2_147_483_647);
|
||||
if (await this.get(id)) id = await this.#getUnusedId();
|
||||
return id;
|
||||
}
|
||||
|
||||
getActiveProfileReferences() {
|
||||
return profiles.values().toArray();
|
||||
}
|
||||
|
||||
async #getUnusedUsername() {
|
||||
const adjective = NameDictionary.Adjectives[Math.floor(Math.random() * NameDictionary.Adjectives.length)];
|
||||
@@ -31,20 +31,28 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
if (await this.getByUsername(username)) username = await this.#getUnusedUsername();
|
||||
return username;
|
||||
}
|
||||
async #getUsernameDefault(username: string) {
|
||||
const prof = await this.getByUsername(username);
|
||||
if (!prof) return username;
|
||||
else return await this.#getUnusedUsername();
|
||||
}
|
||||
|
||||
async create(username?: string) {
|
||||
async create(platform: PlatformType, platformId: string, username?: string) {
|
||||
const id = await this.#getUnusedId();
|
||||
const newUsername = username? username : await this.#getUnusedUsername();
|
||||
const newUsername = username ? await this.#getUsernameDefault(username) : await this.#getUnusedUsername();
|
||||
const newProfile: RecNetAccount = {
|
||||
accountId: id,
|
||||
username: newUsername,
|
||||
displayName: newUsername,
|
||||
platforms: PlatformMask.None,
|
||||
profileImage: "DefaultProfileImage.png",
|
||||
createdAt: new Date()
|
||||
}
|
||||
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, id ], newProfile);
|
||||
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, newUsername ], id);
|
||||
|
||||
await this.server.Platforms.addCachedLogin(platform, platformId, id);
|
||||
|
||||
return this.get(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { RoomInstance } from "../instances/base.ts";
|
||||
import KV from "../persistence/kv.ts";
|
||||
import { ProfileRole } from "../platforms/base.ts";
|
||||
import { PlatformMask, ProfileRole } from "../platforms/types.ts";
|
||||
import { type ServerBase } from "../server.ts";
|
||||
import { type SignalRSocketHandler } from "../socket/signalr/socket.ts";
|
||||
import { ProfileAvatarManager } from "./content/Avatar.ts";
|
||||
import ProfileContentManager from "./content/base.ts";
|
||||
import { ProfileMatchmakingManager } from "./content/Matchmaking.ts";
|
||||
import { ProfileSettingsManager } from "./content/Settings.ts";
|
||||
import ProfileManagerBase from "./manager.ts";
|
||||
import { recNetAccountSchema, SelfAccount, type RecNetAccount } from "./types/profile.ts";
|
||||
@@ -13,12 +16,16 @@ class Profile {
|
||||
#kv: KV;
|
||||
|
||||
#socket: SignalRSocketHandler | null = null;
|
||||
#server: ServerBase;
|
||||
#instance: RoomInstance | null = null;
|
||||
|
||||
#server: ServerBase;
|
||||
#selfAcc: SelfAccount;
|
||||
|
||||
managers: ProfileContentManager[] = [];
|
||||
|
||||
Settings: ProfileSettingsManager;
|
||||
Avatar: ProfileAvatarManager;
|
||||
Matchmaking: ProfileMatchmakingManager;
|
||||
|
||||
constructor(acc: SelfAccount, kv: KV, server: ServerBase) {
|
||||
this.#id = acc.accountId;
|
||||
@@ -28,6 +35,7 @@ class Profile {
|
||||
|
||||
this.Settings = new ProfileSettingsManager(this, this.#kv);
|
||||
this.Avatar = new ProfileAvatarManager(this, this.#kv);
|
||||
this.Matchmaking = new ProfileMatchmakingManager(this, this.#kv);
|
||||
}
|
||||
|
||||
async #saveSelfAcc() {
|
||||
@@ -67,11 +75,12 @@ class Profile {
|
||||
async setBio(bio: string) {
|
||||
const key = this.constructProfilePropertyKey('bio');
|
||||
await this.#kv.getKv().set(key, bio);
|
||||
this.#server.emit('profile.update', { profile: this });
|
||||
}
|
||||
|
||||
async getRole(): Promise<ProfileRole> {
|
||||
const val = (await this.#kv.getKv().get<ProfileRole>(this.constructProfilePropertyKey('role'))).value;
|
||||
if (!val) return ProfileRole.User;
|
||||
if (!val) return ProfileRole.Game;
|
||||
else return val;
|
||||
}
|
||||
|
||||
@@ -80,6 +89,42 @@ class Profile {
|
||||
this.#server.emit('profile.roleupdate', { profile: this, newRole: role });
|
||||
}
|
||||
|
||||
async addPlatform(type: PlatformMask) {
|
||||
const platforms = this.#server.Platforms.getPlatformMask(this.#selfAcc.platforms);
|
||||
this.#selfAcc.platforms = this.#server.Platforms.buildPlatformMask(...[...platforms, type]);
|
||||
await this.#saveSelfAcc();
|
||||
}
|
||||
|
||||
async removePlatform(type: PlatformMask) {
|
||||
const platforms = new Set(this.#server.Platforms.getPlatformMask(this.#selfAcc.platforms));
|
||||
platforms.delete(type);
|
||||
this.#selfAcc.platforms = this.#server.Platforms.buildPlatformMask(...platforms.values().toArray());
|
||||
await this.#saveSelfAcc();
|
||||
}
|
||||
|
||||
async setProfileImg(img: string) {
|
||||
this.#selfAcc.profileImage = img;
|
||||
await this.#saveSelfAcc();
|
||||
}
|
||||
getProfileImg() {
|
||||
return this.#selfAcc.profileImage;
|
||||
}
|
||||
|
||||
getCreationDate() {
|
||||
return this.#selfAcc.createdAt;
|
||||
}
|
||||
|
||||
getPlatforms() {
|
||||
return this.#selfAcc.platforms;
|
||||
}
|
||||
|
||||
getInstance() {
|
||||
return this.#instance;
|
||||
}
|
||||
setInstance(inst: RoomInstance) {
|
||||
this.#instance = inst;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.#id;
|
||||
}
|
||||
@@ -102,6 +147,10 @@ class Profile {
|
||||
return this.#selfAcc;
|
||||
}
|
||||
|
||||
same(profile: Profile) {
|
||||
return profile.getId() == this.getId();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Profile;
|
||||
@@ -1,12 +1,21 @@
|
||||
import z from "zod";
|
||||
import { ProfileRole } from "../../platforms/base.ts";
|
||||
import { PlatformMask } from "../../platforms/types.ts";
|
||||
import Server from "../../server.ts";
|
||||
|
||||
export const recNetAccountSchema = z.object({
|
||||
accountId: z.number(),
|
||||
profileImage: z.string(),
|
||||
isJunior: z.optional(z.boolean()),
|
||||
isJunior: z.coerce.boolean().optional(),
|
||||
username: z.string(),
|
||||
displayName: z.string(),
|
||||
platforms: z.number().transform(arg => {
|
||||
try {
|
||||
Server.Platforms.getPlatformMask(arg);
|
||||
return arg;
|
||||
} catch {
|
||||
return PlatformMask.All;
|
||||
}
|
||||
}),
|
||||
createdAt: z.union([ z.date(), z.string().transform((arg, ctx) => {
|
||||
const d = new Date(arg);
|
||||
if (isNaN(d.getTime())) {
|
||||
@@ -24,14 +33,4 @@ export const selfAccountSchema = recNetAccountSchema.extend({
|
||||
});
|
||||
|
||||
export type RecNetAccount = z.infer<typeof recNetAccountSchema>;
|
||||
export type SelfAccount = z.infer<typeof selfAccountSchema>;
|
||||
|
||||
export const profileTokenSchema = z.object({
|
||||
iss: z.string(),
|
||||
exp: z.number().min(Date.now()),
|
||||
iat: z.number().min(Date.now()),
|
||||
sub: z.number(),
|
||||
role: z.enum(ProfileRole)
|
||||
});
|
||||
|
||||
export type ProfileToken = z.infer<typeof profileTokenSchema>;
|
||||
export type SelfAccount = z.infer<typeof selfAccountSchema>;
|
||||
@@ -1,15 +1,19 @@
|
||||
import { AvatarContentBase } from "./avatars/base.ts";
|
||||
import { EventManager } from "./baseevent.ts";
|
||||
import { CommandsBase } from "./commands/commands.ts";
|
||||
import { ServerContentManager } from "./content/base.ts";
|
||||
import GameConfigsBase from "./gameconfigs/base.ts";
|
||||
import { InstanceManager } from "./instances/base.ts";
|
||||
import { PlatformsManager } from "./platforms/base.ts";
|
||||
import { type PresenceUpdateEvent } from "./presence/events/PresenceUpdateEvent.ts";
|
||||
import { type ProfileUpdateEvent } from "./profiles/events/ProfileUpdate.ts";
|
||||
import { type RoleUpdateEvent } from "./profiles/events/RoleUpdate.ts";
|
||||
import ProfileManagerBase from "./profiles/manager.ts";
|
||||
|
||||
interface ServerEvents {
|
||||
'profile.roleupdate': RoleUpdateEvent,
|
||||
'profile.update': ProfileUpdateEvent
|
||||
'profile.update': ProfileUpdateEvent,
|
||||
'presence.update': PresenceUpdateEvent,
|
||||
}
|
||||
|
||||
class ServerBase extends EventManager<ServerEvents> {
|
||||
@@ -18,6 +22,8 @@ class ServerBase extends EventManager<ServerEvents> {
|
||||
Commands = new CommandsBase(this, 'commands');
|
||||
Platforms = new PlatformsManager(this, 'platforms', true);
|
||||
Avatars = new AvatarContentBase(this, 'avatars');
|
||||
Instances = new InstanceManager(this, 'instances');
|
||||
Content = new ServerContentManager(this, "content");
|
||||
}
|
||||
|
||||
const Server = new ServerBase();
|
||||
|
||||
@@ -72,7 +72,7 @@ export class SignalRSocketHandler {
|
||||
this.sendRaw({});
|
||||
return;
|
||||
} else {
|
||||
if (logmessages) this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`);
|
||||
if (logmessages) this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n Content: ${JSON.stringify(message.data)}`);
|
||||
if (message.data.type == SignalMessageType.Invocation && message.data.invocationId) { // don't send completion messages for nonblocking invocations
|
||||
const res = await this.#dispatchTarget(message.data.target, message.data.arguments[0]); // rec room only uses the first index
|
||||
if (res.type == TargetResultType.Success) {
|
||||
|
||||
@@ -5,7 +5,9 @@ export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
||||
|
||||
#ids: number[] = [];
|
||||
|
||||
override zod = z.tuple([]).rest(z.number());
|
||||
override zod = z.object({
|
||||
PlayerIds: z.array(z.number().nonnegative().max(2_147_483_647))
|
||||
});
|
||||
|
||||
override exec(...ids: number[]) {
|
||||
this.#ids = ids;
|
||||
|
||||
@@ -5,7 +5,7 @@ export class SocketTarget {
|
||||
|
||||
socket: SignalRSocketHandler;
|
||||
|
||||
zod: z.ZodTuple = z.tuple([]);
|
||||
zod: z.ZodObject = z.object({});
|
||||
|
||||
constructor(socket: SignalRSocketHandler) {
|
||||
this.socket = socket;
|
||||
|
||||
@@ -150,14 +150,13 @@ export enum PushNotificationId {
|
||||
RelationshipChanged = 1,
|
||||
MessageReceived,
|
||||
MessageDeleted,
|
||||
PresenceHeartbeatResponse,
|
||||
PresenceHeartbeatResponse, // unused by the game
|
||||
RefreshLogin,
|
||||
Logout,
|
||||
SubscriptionUpdateProfile = 11,
|
||||
SubscriptionUpdatePresence,
|
||||
SubscriptionUpdateGameSession,
|
||||
SubscriptionUpdateRoom = 15,
|
||||
SubscriptionUpdateRoomPlaylist,
|
||||
ModerationQuitGame = 20,
|
||||
ModerationUpdateRequired,
|
||||
ModerationKick,
|
||||
@@ -166,7 +165,6 @@ export enum PushNotificationId {
|
||||
ServerMaintenance,
|
||||
GiftPackageReceived = 30,
|
||||
GiftPackageReceivedImmediate,
|
||||
GiftPackageRewardSelectionReceived,
|
||||
ProfileJuniorStatusUpdate = 40,
|
||||
RelationshipsInvalid = 50,
|
||||
StorefrontBalanceAdd = 60,
|
||||
@@ -184,7 +182,4 @@ export enum PushNotificationId {
|
||||
CommunityBoardUpdate = 95,
|
||||
CommunityBoardAnnouncementUpdate,
|
||||
InventionModerationStateChanged = 100,
|
||||
FreeGiftButtonItemsAdded = 110,
|
||||
LocalRoomKeyCreated = 120,
|
||||
LocalRoomKeyDeleted
|
||||
}
|
||||
Reference in New Issue
Block a user