162 lines
5.4 KiB
TypeScript
162 lines
5.4 KiB
TypeScript
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;
|
|
host_id?: string | null;
|
|
host?: Pick<
|
|
import('./hosts.js').Host,
|
|
'id' | 'name' | 'connectionState' | 'connected' | 'lastHeartbeat'
|
|
> | null;
|
|
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;
|
|
capacity?: number | null;
|
|
capacity_observed_at?: string | 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));
|
|
}
|