/** * tosu (https://github.com/tosuapp/tosu) reads the osu! tourney client's * memory and serves it over a websocket. We need each spectated player's * live score, and how far through the map they are. */ export interface TosuClientScore { osuId: number; name: string; score: number; /** Percent. */ accuracy: number; combo: number; failed: boolean; } export interface TosuFrame { /** 1 idle, 2 waiting for clients, 3 playing, 4 results (tosu's ipcState). */ ipcState: number | null; beatmapId: number | null; /** 0–1 through the map. */ progress: number | null; clients: TosuClientScore[]; } /** Picks what Vash needs out of one tosu v2 message. Returns null outside tourney mode. */ export function parseTosu(message: unknown): TosuFrame | null { const root = message as { tourney?: { ipcState?: number; clients?: { user?: { id?: number; name?: string }; play?: { score?: number; accuracy?: number; failed?: boolean; combo?: { current?: number }; }; }[]; }; beatmap?: { id?: number; time?: { live?: number; firstObject?: number; lastObject?: number } }; }; if (!root?.tourney || !Array.isArray(root.tourney.clients)) return null; const time = root.beatmap?.time; const span = time && time.lastObject != null && time.firstObject != null ? time.lastObject - time.firstObject : 0; return { ipcState: root.tourney.ipcState ?? null, beatmapId: root.beatmap?.id ?? null, progress: span > 0 && time?.live != null ? Math.max(0, Math.min(1, (time.live - time.firstObject!) / span)) : null, clients: root.tourney.clients .filter((client) => (client.user?.id ?? 0) > 0) .map((client) => ({ osuId: client.user!.id!, name: client.user?.name ?? '', score: client.play?.score ?? 0, accuracy: client.play?.accuracy ?? 0, combo: client.play?.combo?.current ?? 0, failed: client.play?.failed ?? false })) }; }