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_END = /ExitLevel:\s*executed/;
|
||||
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
|
||||
// it so we can back-compute a wall-clock game_started_at from the most recent
|
||||
// line — this stays accurate across process restarts and matches what the
|
||||
// game itself reports (e.g. `83:55 ExitLevel: executed`).
|
||||
// it so we can back-compute a wall-clock game_started_at that stays accurate
|
||||
// across restarts and matches what the game itself reports (e.g. `102:47 J;...`).
|
||||
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:
|
||||
* - `in-progress`: the most recent InitGame in the tail has no ShutdownGame
|
||||
* or ExitLevel after it. Emits `startedAt` = now - MM:SS_of_last_line.
|
||||
* - `in-progress`: the most recent InitGame has at least one J; join 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,
|
||||
* 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 ->
|
||||
* ShutdownGame at 0:00 with no ExitLevel between them). No timer.
|
||||
* - `idle`: no join line has fired inside the current InitGame cycle (empty
|
||||
* map loaded, or completed idle cycle at 0:00). No timer.
|
||||
* - `no-signal`: log file missing / no InitGame anywhere in the tail —
|
||||
* 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() };
|
||||
}
|
||||
|
||||
// In progress. Back-compute the wall-clock start from the most recent
|
||||
// line's MM:SS prefix. Walk backwards from the tail to find the last line
|
||||
// with a parseable time — falls back to the InitGame line itself (0:00).
|
||||
let elapsedMs = 0;
|
||||
for (let i = lines.length - 1; i > lastInit; i--) {
|
||||
const t = parseTimePrefix(lines[i]);
|
||||
if (t !== null) {
|
||||
elapsedMs = t;
|
||||
// In progress from InitGame's POV — but only anchor the game timer once a
|
||||
// real player has joined. Find the FIRST J; line after lastInit.
|
||||
let firstJoinIdx = -1;
|
||||
for (let i = lastInit + 1; i < lines.length; i++) {
|
||||
if (MATCH_JOIN.test(lines[i])) {
|
||||
firstJoinIdx = i;
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user