import Logging from "@proxnet/undead-logging"; import { ServerContentBase } from "../ContentBase.ts"; 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"; const log = new Logging("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; #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.#server.emit('presence.update', { profile: this.#profile, presence: this }); } setStatusVisibility(sv: PlayerStatusVisibility) { 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() { return this.#lastExported; } logout() { this.updateLastSeen(); this.#statusVisibility = PlayerStatusVisibility.Offline; } export() { log.d(`profId ${this.#profile.getId()} presence exported. has instance: ${this.#profile.getInstance() !== null}`); this.#lastExported = new Date(); const e: PresenceExport = { playerId: this.#profile.getId(), statusVisibility: this.#statusVisibility, deviceClass: this.#deviceClass, vrMovementMode: this.#vrMovementMove, roomInstance: this.#profile.getInstance()?.export() ?? null } return e; } } export class ServerPresenceBase extends ServerContentBase { #log = new Logging("Presence"); #presenceMap: Map = new Map(); #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(() => { if (this.#presenceMap.size === 0) return; 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() { clearInterval(this.#intervalId ?? undefined); } }