Refresh Fleet with discovery, operation progress, and lobby watches

This commit is contained in:
2026-09-18 11:59:14 -04:00
parent ef8ff69d17
commit 9cf0bd87f4
14 changed files with 761 additions and 240 deletions
+107
View File
@@ -0,0 +1,107 @@
import { parseServerName, zombieMapBySlug } from './map-aliases.js';
import type { ZombieGame } from './map-aliases.js';
import type { RoundTelemetry } from './telemetry.js';
import type { Health, RosterPlayer } from './ui/stores/servers.js';
import { mapTitle, serverTitle } from './ui/server-labels.js';
export interface FleetServer {
name: string;
game: string | null;
map: string | null;
map_slug?: string | null;
game_mode?: string | null;
last_players: number | null;
last_map: string | null;
last_gametype: string | null;
last_seen_at: string | null;
last_error: string | null;
roster: RosterPlayer[] | null;
health: Health;
held: { target: string; betaId: number }[];
active_since: string | null;
inactive_since: string | null;
game_started_at: string | null;
game_ended_at: string | null;
}
export interface FleetOperation {
key: string;
group: string;
server: string;
kind: 'campaign' | 'deployment' | 'beta' | 'recovery';
label: string;
detail: string;
href: string;
state: 'active' | 'attention' | 'done';
startedAt: string;
}
export type FleetFilter = 'all' | 'occupied' | 'empty' | 'attention' | 'testing' | 'pinned';
export type FleetSort = 'name' | 'players' | 'round' | 'duration';
export function freshServer(server: FleetServer, now: number): boolean {
const age = now - Date.parse(server.last_seen_at ?? '');
return server.health === 'online' && age >= 0 && age <= 30_000;
}
export function fleetIdentity(server: FleetServer) {
const parsed = parseServerName(server.name);
const game = server.game ?? parsed?.game ?? '';
const mode = server.game_mode ?? parsed?.gameMode;
const selection = !mode || mode === 'zombie'
? zombieMapBySlug(game, server.map_slug ?? parsed?.mapSlug ?? '') : undefined;
const map = server.last_map ?? server.map;
// A live map change must not keep the registered map's launch shortcut.
const matchingSelection = selection && (!map || selection.canonical === map) ? selection : undefined;
return {
game,
title: serverTitle(server.name),
map: matchingSelection?.friendly ?? mapTitle(map),
group: `${game}:${matchingSelection?.slug ?? map ?? 'unknown'}`,
launch: matchingSelection && (game === 't5' || game === 't6')
? { game: game as ZombieGame, slug: matchingSelection.slug } : null
};
}
export function currentRound(server: FleetServer, report: RoundTelemetry | undefined, now: number): number | null {
if (!report || !freshServer(server, now) || !server.active_since || !server.game_started_at
|| server.game_ended_at || (server.last_players ?? 0) === 0) return null;
const received = Date.parse(report.receivedAt);
const age = now - received;
return age >= 0 && age <= 120_000 && received >= Date.parse(server.game_started_at)
&& received >= Date.parse(server.active_since) ? report.round : null;
}
export function matchesFleet(server: FleetServer, query: string, filter: FleetFilter, game: string,
pinned: readonly string[], operations: readonly FleetOperation[], now: number): boolean {
const identity = fleetIdentity(server);
if (game && identity.game !== game) return false;
const fresh = freshServer(server, now);
if (filter === 'occupied' && !(fresh && (server.last_players ?? 0) > 0)) return false;
if (filter === 'empty' && !(fresh && server.last_players === 0)) return false;
if (filter === 'attention' && !(server.health === 'offline' || server.health === 'stale'
|| (server.health === 'online' && !fresh) || operations.some(op => op.server === server.name && op.state === 'attention'))) return false;
if (filter === 'testing' && !server.held.length) return false;
if (filter === 'pinned' && !pinned.includes(server.name)) return false;
const text = [server.name, identity.title, identity.map, identity.game,
...(server.roster ?? []).map(player => player.name)].join(' ').toLocaleLowerCase();
return query.trim().toLocaleLowerCase().split(/\s+/).every(term => text.includes(term));
}
export interface LobbyWatch {
id: string;
server: string;
kind: 'empty' | 'online' | 'operation';
operationKey?: string;
href: string;
}
export function watchSatisfied(watch: LobbyWatch, server: FleetServer | undefined,
operations: readonly FleetOperation[], now: number): boolean {
if (watch.kind === 'operation') {
// Disappearance, failure and replacement by another operation are not completion.
return operations.some(op => op.key === watch.operationKey && op.state === 'done');
}
if (!server || !freshServer(server, now)) return false;
return watch.kind === 'online' || server.last_players === 0;
}