Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
+86
-28
@@ -45,12 +45,20 @@ function logSeconds(time: string): number {
|
||||
}
|
||||
|
||||
function plainText(value: string): string {
|
||||
return value.replace(/\^[0-9]/g, '').replace(/[\x00-\x1f\x7f]/g, '').trim();
|
||||
return (
|
||||
value
|
||||
.replace(/\^[0-9]/g, '')
|
||||
// eslint-disable-next-line no-control-regex -- Strip control characters from displayed chat text.
|
||||
.replace(/[\x00-\x1f\x7f]/g, '')
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
/** Preserve log order: elapsed timestamps can reset between matches. */
|
||||
export function parseGameChat(lines: readonly string[]): ChatMessage[] {
|
||||
return parseGameChatTimeline(lines).flatMap(entry => entry.type === 'message' ? [entry.message] : []);
|
||||
return parseGameChatTimeline(lines).flatMap((entry) =>
|
||||
entry.type === 'message' ? [entry.message] : []
|
||||
);
|
||||
}
|
||||
|
||||
/** Keep match boundaries alongside messages, including when the log clock resets. */
|
||||
@@ -89,8 +97,16 @@ function indexedTimeline(lines: readonly string[]): { entry: ChatEntry; lineInde
|
||||
if (!match) continue;
|
||||
const message = plainText(match[5]);
|
||||
if (!message) continue;
|
||||
push({ type: 'message', message: { time: match[1], guid: match[2], slot: Number(match[3]),
|
||||
player: plainText(match[4]) || 'Unknown player', message } });
|
||||
push({
|
||||
type: 'message',
|
||||
message: {
|
||||
time: match[1],
|
||||
guid: match[2],
|
||||
slot: Number(match[3]),
|
||||
player: plainText(match[4]) || 'Unknown player',
|
||||
message
|
||||
}
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
@@ -100,11 +116,14 @@ function indexedTimeline(lines: readonly string[]): { entry: ChatEntry; lineInde
|
||||
* segment to the file's last write, then join observed gameplay at that time.
|
||||
* Resets have unknown downtime, so earlier segments cannot share this anchor.
|
||||
*/
|
||||
export function enrichGameChatTimeline(lines: readonly string[], options: {
|
||||
mtime: string | null;
|
||||
matches: readonly ChatMatch[];
|
||||
reports: readonly ChatRoundReport[];
|
||||
}): ContextualChatEntry[] {
|
||||
export function enrichGameChatTimeline(
|
||||
lines: readonly string[],
|
||||
options: {
|
||||
mtime: string | null;
|
||||
matches: readonly ChatMatch[];
|
||||
reports: readonly ChatRoundReport[];
|
||||
}
|
||||
): ContextualChatEntry[] {
|
||||
let segmentStart = 0;
|
||||
let previousSeconds: number | null = null;
|
||||
let lastLine = -1;
|
||||
@@ -115,30 +134,43 @@ export function enrichGameChatTimeline(lines: readonly string[], options: {
|
||||
const event = line.slice(timestamp?.[0].length ?? 0).trimStart();
|
||||
const lifecycle = /^(?:InitGame|ExitLevel|ShutdownGame)(?:\s|:|$)/.test(event);
|
||||
if (!timestamp) {
|
||||
if (lifecycle) { segmentStart = index + 1; previousSeconds = null; }
|
||||
if (lifecycle) {
|
||||
segmentStart = index + 1;
|
||||
previousSeconds = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const seconds = logSeconds(timestamp[1]);
|
||||
if (previousSeconds !== null && (seconds < previousSeconds
|
||||
|| (seconds === 0 && /^InitGame(?:\s|:|$)/.test(event)))) segmentStart = index;
|
||||
if (
|
||||
previousSeconds !== null &&
|
||||
(seconds < previousSeconds || (seconds === 0 && /^InitGame(?:\s|:|$)/.test(event)))
|
||||
)
|
||||
segmentStart = index;
|
||||
previousSeconds = seconds;
|
||||
}
|
||||
// An untimed final write cannot locate the preceding timed event in real time.
|
||||
const finalTimestamp = lastLine < 0 ? null : logTimestamp(lines[lastLine]);
|
||||
const writtenAt = options.mtime ? Date.parse(options.mtime) : NaN;
|
||||
const offset = finalTimestamp ? writtenAt - logSeconds(finalTimestamp[1]) * 1000 : NaN;
|
||||
const reports = options.reports.map(report => ({ ...report, at: Date.parse(report.receivedAt) }))
|
||||
const reports = options.reports
|
||||
.map((report) => ({ ...report, at: Date.parse(report.receivedAt) }))
|
||||
.sort((a, b) => b.at - a.at);
|
||||
const matches = options.matches.map(match => ({ ...match, start: Date.parse(match.startedAt) }))
|
||||
const matches = options.matches
|
||||
.map((match) => ({ ...match, start: Date.parse(match.startedAt) }))
|
||||
.sort((a, b) => b.start - a.start)
|
||||
.map((match, index, sorted) => {
|
||||
const nextStart = sorted[index - 1]?.start ?? Infinity;
|
||||
// Identical heartbeats are deduplicated by the leaderboard, but still
|
||||
// establish that this match was running when the message was sent.
|
||||
const latest = reports.find(report => report.at >= match.start && report.at < nextStart);
|
||||
const latest = reports.find(
|
||||
(report) => report.at >= match.start && report.at < nextStart
|
||||
);
|
||||
const lastSeen = Math.max(Date.parse(match.lastReceivedAt), latest?.at ?? -Infinity);
|
||||
const end = match.endedAt ? Date.parse(match.endedAt)
|
||||
: match.status === 'active' ? lastSeen + 120_000 : Date.parse(match.lastReceivedAt);
|
||||
const end = match.endedAt
|
||||
? Date.parse(match.endedAt)
|
||||
: match.status === 'active'
|
||||
? lastSeen + 120_000
|
||||
: Date.parse(match.lastReceivedAt);
|
||||
return { ...match, end: Math.min(end, nextStart) };
|
||||
});
|
||||
let logStartedAt: number | null = null;
|
||||
@@ -147,8 +179,10 @@ export function enrichGameChatTimeline(lines: readonly string[], options: {
|
||||
return indexedTimeline(lines).map(({ entry, lineIndex }) => {
|
||||
const context: ChatContext = { realTime: null, matchElapsedMs: null, round: null };
|
||||
const time = entry.type === 'message' ? entry.message.time : entry.time;
|
||||
const at = time !== null && lineIndex >= segmentStart ? offset + logSeconds(time) * 1000 : NaN;
|
||||
if (Number.isFinite(at) && Number.isFinite(new Date(at).getTime())) context.realTime = new Date(at).toISOString();
|
||||
const at =
|
||||
time !== null && lineIndex >= segmentStart ? offset + logSeconds(time) * 1000 : NaN;
|
||||
if (Number.isFinite(at) && Number.isFinite(new Date(at).getTime()))
|
||||
context.realTime = new Date(at).toISOString();
|
||||
if (entry.type === 'boundary' && entry.event !== 'end') {
|
||||
// The first event after an implicit reset can be several seconds into
|
||||
// the new process. Its clock origin is the lower bound, not that event.
|
||||
@@ -156,20 +190,44 @@ export function enrichGameChatTimeline(lines: readonly string[], options: {
|
||||
logStartedAt = context.realTime ? (entry.event === 'reset' ? offset : at) : Infinity;
|
||||
logEnded = false;
|
||||
}
|
||||
const match = context.realTime && !logEnded ? matches.find(match => at >= match.start && at <= match.end
|
||||
// Allow the log's one-second precision at a start, but never cross a later InitGame.
|
||||
&& (logStartedAt === null || match.start + 1000 >= logStartedAt
|
||||
|| reports.some(report => report.event === 'game_start' && report.playerCount > 0
|
||||
&& report.at >= match.start && report.at <= at && report.at + 1000 >= logStartedAt!))) : undefined;
|
||||
const match =
|
||||
context.realTime && !logEnded
|
||||
? matches.find(
|
||||
(match) =>
|
||||
at >= match.start &&
|
||||
at <= match.end &&
|
||||
// Allow the log's one-second precision at a start, but never cross a later InitGame.
|
||||
(logStartedAt === null ||
|
||||
match.start + 1000 >= logStartedAt ||
|
||||
reports.some(
|
||||
(report) =>
|
||||
report.event === 'game_start' &&
|
||||
report.playerCount > 0 &&
|
||||
report.at >= match.start &&
|
||||
report.at <= at &&
|
||||
report.at + 1000 >= logStartedAt!
|
||||
))
|
||||
)
|
||||
: undefined;
|
||||
if (match) {
|
||||
const report = reports.find(report => report.at <= at && report.at >= match.start);
|
||||
const report = reports.find((report) => report.at <= at && report.at >= match.start);
|
||||
if (!report || (report.event !== 'game_end' && report.playerCount > 0)) {
|
||||
// A repeated start updates the fleet timer even if the leaderboard
|
||||
// deduplicates it. Use that same observed start for later messages.
|
||||
const start = reports.find(report => report.event === 'game_start' && report.playerCount > 0
|
||||
&& report.at >= match.start && report.at <= at)?.at ?? (match.startObserved ? match.start : null);
|
||||
const start =
|
||||
reports.find(
|
||||
(report) =>
|
||||
report.event === 'game_start' &&
|
||||
report.playerCount > 0 &&
|
||||
report.at >= match.start &&
|
||||
report.at <= at
|
||||
)?.at ?? (match.startObserved ? match.start : null);
|
||||
if (start !== null) context.matchElapsedMs = at - start;
|
||||
if (report && at - report.at <= 120_000 && (report.event === 'round_change' || report.event === 'heartbeat')) {
|
||||
if (
|
||||
report &&
|
||||
at - report.at <= 120_000 &&
|
||||
(report.event === 'round_change' || report.event === 'heartbeat')
|
||||
) {
|
||||
context.round = report.round;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user