Anchor game timer to first player join, not InitGame
An idle map load emits InitGame in games_sp.log without anyone joining, which was falsely surfacing the game timer on empty servers. Track the first `J;<guid>;<slot>;<name>` line inside each InitGame cycle instead and back-compute the wall-clock start from that line's in-game MM:SS. Removes the coarse client-side activeSince gate since the scanner is now the source of truth.
This commit is contained in:
@@ -12,10 +12,14 @@ import { resolveLogPath, tailLog } from './logs.js';
|
|||||||
export const MATCH_START = /InitGame:/;
|
export const MATCH_START = /InitGame:/;
|
||||||
export const MATCH_END = /ExitLevel:\s*executed/;
|
export const MATCH_END = /ExitLevel:\s*executed/;
|
||||||
export const MATCH_SHUTDOWN = /ShutdownGame:/;
|
export const MATCH_SHUTDOWN = /ShutdownGame:/;
|
||||||
|
// Player join line: `MM:SS J;<guid>;<slot>;<name>`. Every joining player emits
|
||||||
|
// one — the first J; line after an InitGame is when the first player actually
|
||||||
|
// entered the match, which is what "game time" should anchor to. An empty map
|
||||||
|
// load still emits InitGame but no J; lines follow.
|
||||||
|
export const MATCH_JOIN = /\sJ;[^;]*;\d+;/;
|
||||||
// Every log line starts with a whitespace-padded MM:SS in-game time. Capture
|
// Every log line starts with a whitespace-padded MM:SS in-game time. Capture
|
||||||
// it so we can back-compute a wall-clock game_started_at from the most recent
|
// it so we can back-compute a wall-clock game_started_at that stays accurate
|
||||||
// line — this stays accurate across process restarts and matches what the
|
// across restarts and matches what the game itself reports (e.g. `102:47 J;...`).
|
||||||
// game itself reports (e.g. `83:55 ExitLevel: executed`).
|
|
||||||
const TIME_PREFIX = /^\s*(\d+):(\d{2})\s/;
|
const TIME_PREFIX = /^\s*(\d+):(\d{2})\s/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,12 +61,13 @@ export interface MatchScanResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the games_sp.log tail and classify the current match state:
|
* Read the games_sp.log tail and classify the current match state:
|
||||||
* - `in-progress`: the most recent InitGame in the tail has no ShutdownGame
|
* - `in-progress`: the most recent InitGame has at least one J; join line
|
||||||
* or ExitLevel after it. Emits `startedAt` = now - MM:SS_of_last_line.
|
* after it and no ExitLevel/ShutdownGame after that. Emits `startedAt`
|
||||||
|
* back-computed from the first join line's in-game MM:SS.
|
||||||
* - `ended`: the most recent InitGame is followed by ExitLevel/ShutdownGame,
|
* - `ended`: the most recent InitGame is followed by ExitLevel/ShutdownGame,
|
||||||
* and the shutdown wasn't the trivial 0:00 idle cycle. Emits `endedAt` = now.
|
* and the shutdown wasn't the trivial 0:00 idle cycle. Emits `endedAt` = now.
|
||||||
* - `idle`: the visible cycle was an idle map load (adjacent InitGame ->
|
* - `idle`: no join line has fired inside the current InitGame cycle (empty
|
||||||
* ShutdownGame at 0:00 with no ExitLevel between them). No timer.
|
* map loaded, or completed idle cycle at 0:00). No timer.
|
||||||
* - `no-signal`: log file missing / no InitGame anywhere in the tail —
|
* - `no-signal`: log file missing / no InitGame anywhere in the tail —
|
||||||
* caller should leave persisted timer state alone.
|
* caller should leave persisted timer state alone.
|
||||||
*/
|
*/
|
||||||
@@ -95,18 +100,30 @@ export async function scanMatchState(serverName: string): Promise<MatchScanResul
|
|||||||
return { kind: 'ended', endedAt: new Date().toISOString() };
|
return { kind: 'ended', endedAt: new Date().toISOString() };
|
||||||
}
|
}
|
||||||
|
|
||||||
// In progress. Back-compute the wall-clock start from the most recent
|
// In progress from InitGame's POV — but only anchor the game timer once a
|
||||||
// line's MM:SS prefix. Walk backwards from the tail to find the last line
|
// real player has joined. Find the FIRST J; line after lastInit.
|
||||||
// with a parseable time — falls back to the InitGame line itself (0:00).
|
let firstJoinIdx = -1;
|
||||||
let elapsedMs = 0;
|
for (let i = lastInit + 1; i < lines.length; i++) {
|
||||||
for (let i = lines.length - 1; i > lastInit; i--) {
|
if (MATCH_JOIN.test(lines[i])) {
|
||||||
const t = parseTimePrefix(lines[i]);
|
firstJoinIdx = i;
|
||||||
if (t !== null) {
|
|
||||||
elapsedMs = t;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const startedAt = new Date(Date.now() - elapsedMs).toISOString();
|
if (firstJoinIdx === -1) return { kind: 'idle' };
|
||||||
|
|
||||||
|
// Back-compute the wall-clock start from the delta between the first join
|
||||||
|
// line's in-game time and the most recent parseable line's in-game time.
|
||||||
|
const joinGameTime = parseTimePrefix(lines[firstJoinIdx]) ?? 0;
|
||||||
|
let currentGameTime = joinGameTime;
|
||||||
|
for (let i = lines.length - 1; i > firstJoinIdx; i--) {
|
||||||
|
const t = parseTimePrefix(lines[i]);
|
||||||
|
if (t !== null) {
|
||||||
|
currentGameTime = t;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const elapsedSinceJoin = Math.max(0, currentGameTime - joinGameTime);
|
||||||
|
const startedAt = new Date(Date.now() - elapsedSinceJoin).toISOString();
|
||||||
return { kind: 'in-progress', startedAt };
|
return { kind: 'in-progress', startedAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user