From 57746eef193d73c19e666790dc72b14ea5f557a9 Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 12 Sep 2026 11:19:26 -0400 Subject: [PATCH] Use `status` for the RCON probe (Plutonium doesn't wrap-recognize getstatus) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hex dump surfaced Plutonium's actual reply: ff ff ff ff 70 72 69 6e 74 0a ....print. 55 6e 6b 6e 6f 77 6e 20 63 6f Unknown co 6d 6d 61 6e 64 20 22 67 65 74 mmand "get 73 74 61 74 75 73 22 0a status". Auth was fine — Plutonium just doesn't recognize `getstatus` as an rcon subcommand. In Q3-derived engines, `getstatus` is an anonymous query (no rcon wrapper); we were sending `rcon getstatus`, which made Plutonium treat it as an unknown subcommand. Switch rconStatusResult to use `status`, same command rconGetPlayerCount has been using successfully all along. The response is a `print` body with a `key: value` header (`map: mp_kino\n...`) followed by the scoreboard, not the `\info\` line the statusResponse dialect uses. New parseStatusHeader extracts key:value dvars from the header, stopping at the `--- ---` separator (or the first player row) so guids and names can't leak into the metadata. rconStatusResult now merges header values with the statusResponse info line so both dialects produce map/gametype in the RconStatus shape. Co-Authored-By: Claude Opus 4.7 --- src/lib/server/integrations/rcon.ts | 36 +++++++++++++++++++++---- tests/unit/rcon.test.ts | 41 +++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/lib/server/integrations/rcon.ts b/src/lib/server/integrations/rcon.ts index 79892e5..892d76c 100644 --- a/src/lib/server/integrations/rcon.ts +++ b/src/lib/server/integrations/rcon.ts @@ -157,6 +157,25 @@ export function parseInfoLine(text: string): Record { return out; } +/** + * Parse `key: value` header lines from a `status` (print) response, stopping + * at the scoreboard `--- ---` separator so player rows (which can contain + * colons in guids or names) don't leak into the metadata. Plutonium T5's + * `status` puts things like `map: mp_kino` and (on some builds) gametype + * in the header. + */ +export function parseStatusHeader(text: string): Record { + const out: Record = {}; + for (const rawLine of text.split(/\r?\n/)) { + const line = rawLine.trim(); + if (/^-+\s+-+/.test(line)) break; // scoreboard row — stop + if (/^\d+\s+/.test(line)) break; // player row that snuck in + const m = /^([a-z][a-z0-9_]*)\s*:\s*(.+)$/i.exec(line); + if (m) out[m[1].toLowerCase()] = m[2].trim(); + } + return out; +} + export interface RconPlayer { num: number | null; // client slot — print dialect only score: number; @@ -292,9 +311,13 @@ export async function rconStatusResult(server: string): Promise { }); }); +describe('parseStatusHeader', () => { + it('extracts key:value header lines from a Plutonium status body', () => { + const body = [ + 'print', + 'map: mp_kino', + 'num score ping guid name', + '--- ----- ---- -------- ----', + ' 0 12 45 abc123def456 joe' + ].join('\n'); + const h = parseStatusHeader(body); + expect(h.map).toBe('mp_kino'); + // Header keys stop at the scoreboard — guids/names must not leak in. + expect(h.abc123def456).toBeUndefined(); + }); + + it('handles builds that include gametype or other dvars in the header', () => { + const body = [ + 'print', + 'map: mp_ascension', + 'gametype: zclassic', + 'sv_maxclients: 4', + '--- --- ---', + '' + ].join('\n'); + const h = parseStatusHeader(body); + expect(h.map).toBe('mp_ascension'); + expect(h.gametype).toBe('zclassic'); + expect(h.sv_maxclients).toBe('4'); + }); + + it('returns empty object for a statusResponse (no colon header)', () => { + const body = [ + 'statusResponse', + '\\mapname\\mp_kino\\g_gametype\\zclassic', + '10 45 "joe"' + ].join('\n'); + expect(parseStatusHeader(body)).toEqual({}); + }); +}); + describe('formatRawReply', () => { it('renders a small buffer as hex + printable ASCII with an offset', () => { const buf = Buffer.from([0xff, 0xff, 0xff, 0xff, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x0a]);