import { parseServerName, zombieMapBySlug } from './map-aliases.js'; import { freshTelemetry, type GameObservation } from './server-observation.js'; import { freshStatusTimestamp } from './server-freshness.js'; import type { ZombieGame } from './map-aliases.js'; import type { GameTelemetry } from './telemetry.js'; import type { Health, RosterPlayer } from './ui/stores/servers.js'; import { mapTitle, serverTitle } from './ui/server-labels.js'; export interface FleetServer { plutonium?: import('./plutonium.js').PlutoniumVersion; container_state?: string | null; container_checked_at?: string | null; last_checked_at?: string | null; telemetry_after?: string | null; rcon_status?: GameObservation; telemetry?: GameTelemetry; status_source?: 'telemetry' | 'rcon' | null; 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' | 'maintenance'; label: string; detail: string; href: string; state: 'active' | 'attention' | 'done'; startedAt: string; activityLabel?: string; resultState?: 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 { return server.health === 'online' && !server.last_error && freshStatusTimestamp(server.last_seen_at, now); } 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: GameTelemetry | undefined, now: number): number | null { if (server.telemetry && (!report || Date.parse(server.telemetry.receivedAt) > Date.parse(report.receivedAt))) report = server.telemetry; if (!report || (report.event !== 'round_change' && report.event !== 'heartbeat') || !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); return freshTelemetry(report, now, server.telemetry_after) && 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)); }