Separate chat messages with match dividers

This commit is contained in:
2026-09-20 22:19:19 -04:00
parent 5c5e75526d
commit 50c81456c4
3 changed files with 118 additions and 20 deletions
+42 -8
View File
@@ -6,18 +6,52 @@ export interface ChatMessage {
message: string;
}
export type ChatEntry =
| { type: 'message'; message: ChatMessage }
| { type: 'boundary'; event: 'start' | 'end' | 'reset'; time: string | null };
function plainText(value: string): string {
return value.replace(/\^[0-9]/g, '').replace(/[\x00-\x1f\x7f]/g, '').trim();
}
/** Preserve log order: elapsed timestamps can reset between matches. */
export function parseGameChat(lines: readonly string[]): ChatMessage[] {
return lines.flatMap(line => {
const match = /^\s*(\d+:[0-5]\d)\s+say;([^;]+);(\d+);([^;]*);(.*)$/.exec(line);
if (!match) return [];
const message = plainText(match[5]);
if (!message) return [];
return [{ time: match[1], guid: match[2], slot: Number(match[3]),
player: plainText(match[4]) || 'Unknown player', message }];
});
return parseGameChatTimeline(lines).flatMap(entry => entry.type === 'message' ? [entry.message] : []);
}
/** Keep match boundaries alongside messages, including when the log clock resets. */
export function parseGameChatTimeline(lines: readonly string[]): ChatEntry[] {
const entries: ChatEntry[] = [];
let previousSeconds: number | null = null;
let ended = false;
for (const line of lines) {
const timestamp = /^\s*(\d+:[0-5]\d)\s+/.exec(line);
const time = timestamp?.[1] ?? null;
const seconds = time === null ? null : Number(time.split(':')[0]) * 60 + Number(time.split(':')[1]);
const event = line.slice(timestamp?.[0].length ?? 0).trimStart();
if (/^InitGame(?:\s|:|$)/.test(event)) {
entries.push({ type: 'boundary', event: 'start', time });
ended = false;
} else if (/^(?:ExitLevel|ShutdownGame)(?:\s|:|$)/.test(event)) {
// Engines may emit both end events for the same match.
if (!ended) entries.push({ type: 'boundary', event: 'end', time });
ended = true;
} else if (seconds !== null && previousSeconds !== null && seconds < previousSeconds) {
entries.push({ type: 'boundary', event: 'reset', time });
ended = false;
}
// Untimed lifecycle events also clear the previous match's clock.
if (seconds !== null || /^(?:InitGame|ExitLevel|ShutdownGame)(?:\s|:|$)/.test(event)) {
previousSeconds = seconds;
}
const match = /^\s*(\d+:[0-5]\d)\s+say;([^;]+);(\d+);([^;]*);(.*)$/.exec(line);
if (!match) continue;
const message = plainText(match[5]);
if (!message) continue;
entries.push({ type: 'message', message: { time: match[1], guid: match[2], slot: Number(match[3]),
player: plainText(match[4]) || 'Unknown player', message } });
}
return entries;
}