Files
galvanic-corrosion-rewrite/src/server/presence/base.ts
2025-09-11 19:47:33 -04:00

163 lines
4.7 KiB
TypeScript

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<Profile, Presence> = 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);
}
}