galvanic corrosion rewrite

commit this before something goes horribly wrong
This commit is contained in:
2025-08-12 21:04:52 -04:00
parent 941c8400c0
commit f19552929e
40 changed files with 28149 additions and 73212 deletions

View File

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

View File

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

View File

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

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

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

View File

@@ -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'],

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

View File

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

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

View File

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

View File

@@ -8,6 +8,7 @@ class ProfileContentManager {
constructor(profile: Profile, kv: KV) {
this.profile = profile;
this.kv = kv;
profile.managers.push(this);
}
}

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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