149 lines
5.9 KiB
TypeScript
149 lines
5.9 KiB
TypeScript
import type { GameClientBridgeJsonObject, GameClientBridgeSnapshotResponse } from "../api/types";
|
|
import type {
|
|
ScumCompanionHealthView,
|
|
ScumFlagView,
|
|
ScumOperationsSnapshotView,
|
|
ScumPlayerView,
|
|
ScumSessionView,
|
|
ScumSnapshotCollection,
|
|
ScumSquadView,
|
|
ScumVehicleView
|
|
} from "../contracts/scumOperations";
|
|
import { safeDiagnosticText } from "../utils/safeDiagnosticText";
|
|
|
|
const maxVisibleItems = 50;
|
|
|
|
export function projectScumOperationsSnapshots(snapshots: GameClientBridgeSnapshotResponse[]): ScumOperationsSnapshotView {
|
|
return {
|
|
health: projectHealth(latestPayload(snapshots, "companion.health")),
|
|
sessions: projectCollection(latestSnapshot(snapshots, "online.sessions"), "sessions", projectSession),
|
|
players: projectCollection(latestSnapshot(snapshots, "players"), "players", projectPlayer),
|
|
squads: projectCollection(latestSnapshot(snapshots, "squads"), "squads", projectSquad),
|
|
vehicles: projectCollection(latestSnapshot(snapshots, "vehicles"), "vehicles", projectVehicle),
|
|
flags: projectCollection(latestSnapshot(snapshots, "flags"), "flags", projectFlag)
|
|
};
|
|
}
|
|
|
|
function latestSnapshot(snapshots: GameClientBridgeSnapshotResponse[], type: string): GameClientBridgeSnapshotResponse | undefined {
|
|
return snapshots
|
|
.filter((snapshot) => snapshot.type === type)
|
|
.sort((left, right) => right.observedAt.localeCompare(left.observedAt) || right.sequence - left.sequence)[0];
|
|
}
|
|
|
|
function latestPayload(snapshots: GameClientBridgeSnapshotResponse[], type: string): GameClientBridgeJsonObject | undefined {
|
|
return latestSnapshot(snapshots, type)?.payload;
|
|
}
|
|
|
|
function projectHealth(payload: GameClientBridgeJsonObject | undefined): ScumCompanionHealthView | undefined {
|
|
if (!payload) return undefined;
|
|
const status = readText(payload.status, 20);
|
|
return {
|
|
status: status === "online" || status === "degraded" || status === "offline" ? status : "unknown",
|
|
version: readOptionalText(payload.version, 40),
|
|
observedAt: readOptionalText(payload.observedAt, 64),
|
|
latencyMs: readOptionalNumber(payload.latencyMs, 0, 30000),
|
|
capabilities: readArray(payload.capabilities).map((value) => readText(value, 80)).filter(Boolean).slice(0, 16)
|
|
};
|
|
}
|
|
|
|
function projectCollection<T>(
|
|
snapshot: GameClientBridgeSnapshotResponse | undefined,
|
|
key: string,
|
|
project: (value: unknown) => T | null
|
|
): ScumSnapshotCollection<T> {
|
|
const values = snapshot ? readArray(snapshot.payload[key]) : [];
|
|
return {
|
|
observedAt: snapshot?.observedAt,
|
|
total: values.length,
|
|
items: values.slice(0, maxVisibleItems).map(project).filter((value): value is T => value !== null)
|
|
};
|
|
}
|
|
|
|
function projectSession(value: unknown): ScumSessionView | null {
|
|
const record = readObject(value);
|
|
const sessionId = readText(record?.sessionId, 120);
|
|
const playerName = readText(record?.playerName, 80);
|
|
return sessionId && playerName ? { sessionId, playerName, startedAt: readOptionalText(record?.startedAt, 64) } : null;
|
|
}
|
|
|
|
function projectPlayer(value: unknown): ScumPlayerView | null {
|
|
const record = readObject(value);
|
|
const playerId = readText(record?.playerId, 96);
|
|
const playerName = readText(record?.playerName, 80);
|
|
const status = readText(record?.status, 20);
|
|
return playerId && playerName && status ? {
|
|
playerId,
|
|
playerName,
|
|
status,
|
|
squadId: readOptionalText(record?.squadId, 96),
|
|
pingMs: readOptionalNumber(record?.pingMs, 0, 10000),
|
|
lastSeenAt: readOptionalText(record?.lastSeenAt, 64)
|
|
} : null;
|
|
}
|
|
|
|
function projectSquad(value: unknown): ScumSquadView | null {
|
|
const record = readObject(value);
|
|
const squadId = readText(record?.squadId, 96);
|
|
const name = readText(record?.name, 80);
|
|
const memberCount = readOptionalNumber(record?.memberCount, 0, 64);
|
|
return squadId && name && memberCount !== undefined ? {
|
|
squadId,
|
|
name,
|
|
memberCount,
|
|
leaderPlayerId: readOptionalText(record?.leaderPlayerId, 96),
|
|
lastActiveAt: readOptionalText(record?.lastActiveAt, 64)
|
|
} : null;
|
|
}
|
|
|
|
function projectVehicle(value: unknown): ScumVehicleView | null {
|
|
const record = readObject(value);
|
|
const vehicleId = readText(record?.vehicleId, 96);
|
|
const vehicleType = readText(record?.vehicleType, 80);
|
|
const status = readText(record?.status, 20);
|
|
return vehicleId && vehicleType && status ? {
|
|
vehicleId,
|
|
vehicleType,
|
|
status,
|
|
ownerPlayerId: readOptionalText(record?.ownerPlayerId, 96),
|
|
squadId: readOptionalText(record?.squadId, 96),
|
|
fuelPercent: readOptionalNumber(record?.fuelPercent, 0, 100),
|
|
healthPercent: readOptionalNumber(record?.healthPercent, 0, 100),
|
|
lastSeenAt: readOptionalText(record?.lastSeenAt, 64)
|
|
} : null;
|
|
}
|
|
|
|
function projectFlag(value: unknown): ScumFlagView | null {
|
|
const record = readObject(value);
|
|
const flagId = readText(record?.flagId, 96);
|
|
const status = readText(record?.status, 20);
|
|
return flagId && status ? {
|
|
flagId,
|
|
status,
|
|
ownerPlayerId: readOptionalText(record?.ownerPlayerId, 96),
|
|
squadId: readOptionalText(record?.squadId, 96),
|
|
radiusMeters: readOptionalNumber(record?.radiusMeters, 0, 5000),
|
|
lastUpdatedAt: readOptionalText(record?.lastUpdatedAt, 64)
|
|
} : null;
|
|
}
|
|
|
|
function readObject(value: unknown): Record<string, unknown> | undefined {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
}
|
|
|
|
function readArray(value: unknown): unknown[] {
|
|
return Array.isArray(value) ? value : [];
|
|
}
|
|
|
|
function readText(value: unknown, maxLength: number): string {
|
|
if (typeof value !== "string") return "";
|
|
return (safeDiagnosticText(value.slice(0, maxLength), "") ?? "").trim();
|
|
}
|
|
|
|
function readOptionalText(value: unknown, maxLength: number): string | undefined {
|
|
return readText(value, maxLength) || undefined;
|
|
}
|
|
|
|
function readOptionalNumber(value: unknown, minimum: number, maximum: number): number | undefined {
|
|
return typeof value === "number" && Number.isFinite(value) && value >= minimum && value <= maximum ? value : undefined;
|
|
}
|