Roster parser: header-driven fixed-width, handle Plutonium T5's 9 columns

The raw hex dump showed Plutonium T5's `status` reply is a 9-column
format, not the 5-column shape the parser assumed:

  num score ping guid    name           lastmsg address              qport rate
  --- ----- ---- ------- ---------------- ------- --------------------- ----- -----
    0  3040   36 2741626 JoshWright^7          0 68.235.46.100:28960  -14733 25000

Three bugs in the old regex: it required 16+ hex chars for guid (T5
uses 7-digit decimal IDs), it expected name to run to end-of-line
(4 trailing columns get glued on), and it left Plutonium color codes
(^0..^9) in the rendered name.

Rewrote parsePlayerRoster to detect column boundaries from the header
line word positions instead — column N runs from word N's start to
word N+1's start, matching how CoD's status output actually lays out
(values borrow the padding between adjacent columns). Same code path
handles the old 5-column format and the T5 9-column format cleanly.

stripColorCodes() removes ^\d sequences from names in both dialects.
PlayerRoster component only appends `…` when the guid is actually
long enough to need truncation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 11:37:25 -04:00
co-authored by Claude Opus 4.7
parent d8ed0d2572
commit d21e8187ff
3 changed files with 130 additions and 18 deletions
+45
View File
@@ -118,6 +118,51 @@ describe('parsePlayerRoster', () => {
expect(parsePlayerRoster(empty)).toEqual([]);
expect(parsePlayerRoster('nonsense')).toEqual([]);
});
it('parses Plutonium T5 9-column status with numeric GUID + color-coded name', () => {
// Verbatim shape from a real T5 status reply — 7-digit GUID (not hex),
// 4 trailing columns after name (lastmsg, address, qport, rate),
// and Plutonium color code (^7) in the name.
const body = [
'print',
'map: zombie_cosmodrome',
'num score ping guid name lastmsg address qport rate',
'--- ----- ---- ------- ---------------- ------- -------------------------- ----- -----',
' 0 3040 36 2741626 JoshWright^7 0 68.235.46.100:28960 -14733 25000'
].join('\n');
const roster = parsePlayerRoster(body);
expect(roster).toHaveLength(1);
expect(roster[0]).toEqual({
num: 0,
score: 3040,
ping: 36,
guid: '2741626',
name: 'JoshWright' // ^7 stripped
});
});
it('preserves multi-word names in the 9-column format', () => {
const body = [
'print',
'map: mp_kino',
'num score ping guid name lastmsg address qport rate',
'--- ----- ---- ------- ---------------- ------- --------------------- ----- -----',
' 1 500 48 8675309 Jenny Two Names 0 10.0.0.5:28960 -12345 25000'
].join('\n');
const roster = parsePlayerRoster(body);
expect(roster).toHaveLength(1);
expect(roster[0].name).toBe('Jenny Two Names');
});
it('strips color codes from statusResponse-dialect names too', () => {
const body = [
'statusResponse',
'\\mapname\\mp_kino\\g_gametype\\zclassic',
'10 45 "^1Red^7Player"'
].join('\n');
const roster = parsePlayerRoster(body);
expect(roster[0].name).toBe('RedPlayer');
});
});
describe('parseStatusHeader', () => {