Files
browser/platform_web/schemas/scumOperations.ts
T
2026-07-20 16:42:33 +08:00

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;
}