This commit is contained in:
scamiv
2026-02-16 13:55:53 +01:00
parent 9f30423b00
commit dd7938a7fc
5 changed files with 52 additions and 10 deletions
+1
View File
@@ -106,6 +106,7 @@ export function buildAnalytics(
archiveDurationSec: lobby.archiveDurationSec,
archiveConnectedPlayers: lobby.archiveConnectedPlayers,
archiveActivePlayers: lobby.archiveActivePlayers,
archiveSpawnedPlayers: lobby.archiveSpawnedPlayers,
scheduledStartAt: lobby.scheduledStartAt,
peakClients: lobby.peakClients,
maxPlayers: lobby.maxPlayers,
+8
View File
@@ -38,6 +38,7 @@ function isBuildIntentType(type: string): boolean {
function deriveReplayParticipationMetrics(archivePayload: unknown): {
connectedPlayers?: number;
activePlayers?: number;
spawnedPlayers?: number;
} {
const root = asRecord(archivePayload);
if (!root) return {};
@@ -47,6 +48,7 @@ function deriveReplayParticipationMetrics(archivePayload: unknown): {
const connectedClientIds = new Set<string>();
const activeClientIds = new Set<string>();
const spawnedClientIds = new Set<string>();
let sawDisconnectedMarker = false;
for (const turnEntry of turns) {
@@ -74,12 +76,16 @@ function deriveReplayParticipationMetrics(archivePayload: unknown): {
if (isBuildIntentType(type)) {
activeClientIds.add(clientID);
}
if (type === "spawn") {
spawnedClientIds.add(clientID);
}
}
}
return {
connectedPlayers: sawDisconnectedMarker ? connectedClientIds.size : undefined,
activePlayers: activeClientIds.size,
spawnedPlayers: spawnedClientIds.size,
};
}
@@ -418,6 +424,7 @@ export class LobbyIngestService {
(record) =>
record.status !== "active" &&
(!record.archiveFound ||
record.archiveSpawnedPlayers === undefined ||
(record.archiveConnectedPlayers === undefined &&
record.archiveActivePlayers === undefined)) &&
!!record.closedAt &&
@@ -579,6 +586,7 @@ export class LobbyIngestService {
players: info?.players?.length,
connectedPlayers: replayMetrics.connectedPlayers,
activePlayers: replayMetrics.activePlayers,
spawnedPlayers: replayMetrics.spawnedPlayers,
durationSec:
typeof info?.duration === "number" ? Math.round(info.duration) : undefined,
winner: winnerLabel,
+2
View File
@@ -327,6 +327,7 @@ export class JsonStore {
players?: number;
connectedPlayers?: number;
activePlayers?: number;
spawnedPlayers?: number;
durationSec?: number;
winner?: string;
lobbyCreatedAt?: number;
@@ -340,6 +341,7 @@ export class JsonStore {
lobby.archivePlayers = payload.players;
lobby.archiveConnectedPlayers = payload.connectedPlayers;
lobby.archiveActivePlayers = payload.activePlayers;
lobby.archiveSpawnedPlayers = payload.spawnedPlayers;
lobby.archiveDurationSec = payload.durationSec;
lobby.archiveWinner = payload.winner;
lobby.actualLobbyCreatedAt = payload.lobbyCreatedAt;
+15 -2
View File
@@ -93,6 +93,7 @@ export interface LobbyRecord {
archivePlayers?: number;
archiveConnectedPlayers?: number;
archiveActivePlayers?: number;
archiveSpawnedPlayers?: number;
archiveDurationSec?: number;
archiveWinner?: string;
actualLobbyCreatedAt?: number;
@@ -166,6 +167,7 @@ export interface AnalyticsPayload {
archiveDurationSec?: number;
archiveConnectedPlayers?: number;
archiveActivePlayers?: number;
archiveSpawnedPlayers?: number;
scheduledStartAt: number;
peakClients: number;
maxPlayers?: number;
@@ -206,8 +208,19 @@ export function safeMaxPlayers(record: Pick<LobbyRecord, "maxPlayers">): number
return Math.max(1, record.maxPlayers ?? 1);
}
export function peakFillRatio(record: Pick<LobbyRecord, "peakClients" | "maxPlayers">): number {
return record.peakClients / safeMaxPlayers(record as Pick<LobbyRecord, "maxPlayers">);
export function peakFillClients(
record: Pick<LobbyRecord, "peakClients" | "archiveConnectedPlayers" | "archivePlayers">,
): number {
return record.archiveConnectedPlayers ?? record.archivePlayers ?? record.peakClients;
}
export function peakFillRatio(
record: Pick<
LobbyRecord,
"peakClients" | "maxPlayers" | "archiveConnectedPlayers" | "archivePlayers"
>,
): number {
return peakFillClients(record) / safeMaxPlayers(record as Pick<LobbyRecord, "maxPlayers">);
}
export function bucketForConfig(
+26 -8
View File
@@ -3,6 +3,7 @@ import {
BucketMode,
LobbyRecord,
TimelineBucket,
peakFillClients,
} from "../shared/types";
import * as d3 from "d3";
import "./styles.css";
@@ -617,7 +618,7 @@ function renderOrder(payload: AnalyticsPayload): void {
<th>Status</th>
<th>Lobby + Game</th>
<th>Peak Fill</th>
<th>Connected / Active</th>
<th>Connected / Active / Spawned</th>
<th>Join/min</th>
<th>Opened</th>
</tr>
@@ -635,7 +636,7 @@ function renderOrder(payload: AnalyticsPayload): void {
</td>
<td class="status-${row.status}">${row.status}</td>
<td>${formatDurationMs(row.openDurationMs)} + ${formatGameDuration(row, payload.now)}</td>
<td>${row.maxPlayers ? `${row.peakClients}/${row.maxPlayers}` : row.peakClients}</td>
<td>${formatPeakFill(row)}</td>
<td>${formatReplayParticipation(row)}</td>
<td>${row.joinRatePerMin.toFixed(2)}</td>
<td>${new Date(row.openedAt).toLocaleString()}</td>
@@ -684,7 +685,7 @@ function renderInteresting(target: "neverStarted" | "lowFill", rows: LobbyRecord
<td class="mono">${row.gameID}</td>
<td>${row.gameConfig?.gameMode ?? "-"}</td>
<td>${row.gameConfig?.gameMap ?? "-"}</td>
<td>${row.maxPlayers ? `${row.peakClients}/${row.maxPlayers}` : row.peakClients}</td>
<td>${formatPeakFill(row)}</td>
<td>${row.fillRatioAtStart !== undefined ? `${(row.fillRatioAtStart * 100).toFixed(1)}%` : "-"}</td>
<td>${formatDurationMs(row.openDurationMs)}</td>
</tr>
@@ -723,20 +724,37 @@ function formatDurationSec(durationSec: number | undefined): string {
function formatReplayParticipation(
row: Pick<
AnalyticsPayload["order"][number],
"archivePlayers" | "archiveConnectedPlayers" | "archiveActivePlayers"
| "archivePlayers"
| "archiveConnectedPlayers"
| "archiveActivePlayers"
| "archiveSpawnedPlayers"
>,
): string {
const connected = row.archiveConnectedPlayers;
const active = row.archiveActivePlayers;
const spawned = row.archiveSpawnedPlayers;
const total = row.archivePlayers;
if (connected === undefined && active === undefined) {
if (connected === undefined && active === undefined && spawned === undefined) {
return "-";
}
const pair = `${connected ?? "-"} / ${active ?? "-"}`;
if (total === undefined) return pair;
return `${pair} of ${total}`;
const triplet = `${connected ?? "-"} / ${active ?? "-"} / ${spawned ?? "-"}`;
if (total === undefined) return triplet;
return `${triplet} of ${total}`;
}
function formatPeakFill(
row: Pick<
AnalyticsPayload["order"][number],
"peakClients" | "maxPlayers" | "archiveConnectedPlayers" | "archivePlayers"
>,
): string {
const peak = peakFillClients(row);
if (row.maxPlayers && row.maxPlayers > 0) {
return `${peak}/${row.maxPlayers}`;
}
return String(peak);
}
function formatGameDuration(