Match RCON player pings by unique name when GUIDs differ

This commit is contained in:
2026-10-06 21:25:03 -04:00
parent 1b42244858
commit 1779ae583b
5 changed files with 83 additions and 8 deletions
+14
View File
@@ -154,6 +154,20 @@ describe('parsePlayerRoster', () => {
expect(roster[0].name).toBe('Jenny Two Names');
});
it('parses T6 status with bot column and right-aligned hexadecimal GUIDs', () => {
const body = [
'map: zm_transit',
'num score bot ping guid name lastmsg address qport rate',
'--- ----- --- ---- -------------------------------- ---------------- ------- --------------------- ------ -----',
' 0 940 0 91 6C4C20 EyelessGreem 50 192.0.2.137:4976 40123 20000',
' 1 150 0 151 6C45DD _frxn.3 50 198.51.100.1:4976 8140 20000'
].join('\n');
expect(parsePlayerRoster(body)).toEqual([
{ num: 0, score: 940, ping: 91, guid: '6C4C20', name: 'EyelessGreem' },
{ num: 1, score: 150, ping: 151, guid: '6C45DD', name: '_frxn.3' }
]);
});
it('strips color codes from statusResponse-dialect names too', () => {
const body = [
'statusResponse',
+36 -3
View File
@@ -52,7 +52,7 @@ describe('shared server observation', () => {
const raw = server({ roster: [
{ ...telemetry.players[1], num: 2, score: 10, ping: 0, name: 'Old name' },
{ ...telemetry.players[0], num: 3, score: 20, ping: 999 },
{ ...telemetry.players[2], num: 4, score: 30, ping: 45, guid: 'different-player' }
{ ...telemetry.players[2], num: 4, score: 30, ping: 45, guid: 'different-player', name: 'Different player' }
] });
const selected = selectServerObservation(server({ rcon_status: raw }), telemetry, now);
expect(selected.roster).toEqual(telemetry.players.map((player, i) => ({
@@ -91,10 +91,9 @@ describe('shared server observation', () => {
expect(selectServerObservation(server({ rcon_status: { ...raw, last_error: 'timeout', rcon_failures: 1 } }), telemetry, now).roster?.[0].ping).toBe(42);
});
it.each(['duplicate', 'missing-guid', 'null', 'negative', 'invalid'])('rejects %s ping matches', mode => {
it.each(['duplicate', 'null', 'negative', 'invalid'])('rejects %s ping matches', mode => {
const telemetry = report();
const player = { ...telemetry.players[0], num: 0, score: 100, ping: 42 as number | null, guid: telemetry.players[0].guid as string | null };
if (mode === 'missing-guid') player.guid = null;
if (mode === 'null') player.ping = null;
if (mode === 'negative') player.ping = -1;
if (mode === 'invalid') player.ping = NaN;
@@ -102,6 +101,40 @@ describe('shared server observation', () => {
expect(selectServerObservation(server({ rcon_status: raw }), telemetry, now).roster?.[0].ping).toBeNull();
});
it('falls back to unique names when RCON GUIDs differ or are missing', () => {
const telemetry = report(0, 2);
telemetry.players[0].name = '^2EyelessGreem^7';
telemetry.players[1].name = ' _frxn.3 ';
const raw = server({ roster: [
{ num: 0, score: 940, ping: 91, guid: '6C4C20', name: 'EyelessGreem' },
{ num: 1, score: 150, ping: 151, guid: null, name: '_frxn.3' }
] });
const selected = selectServerObservation(server({ rcon_status: raw }), telemetry, now);
expect(selected.roster?.map(player => player.ping)).toEqual([91, 151]);
expect(selected.roster?.[0]).toMatchObject({ name: '^2EyelessGreem^7', guid: telemetry.players[0].guid, score: 5000 });
expect(selectServerObservation(server({ rcon_status: { ...raw, last_seen_at: at(-90_001) } }), telemetry, now).roster?.map(player => player.ping)).toEqual([null, null]);
});
it.each(['rcon', 'telemetry', 'case', 'blank', 'invalid-ping'])('rejects ambiguous or invalid %s name fallback', mode => {
const telemetry = report(0, 2);
const player = { num: 0, score: 100, ping: 42, guid: 'different-guid', name: telemetry.players[0].name };
if (mode === 'telemetry') telemetry.players[1].name = '^1' + telemetry.players[0].name;
if (mode === 'case') player.name = player.name.toLowerCase();
if (mode === 'blank') { player.name = ' '; telemetry.players[0].name = '^1'; }
if (mode === 'invalid-ping') player.ping = -1;
const raw = server({ roster: mode === 'rcon' ? [player, { ...player, guid: 'another-guid', name: '^2' + player.name }] : [player] });
expect(selectServerObservation(server({ rcon_status: raw }), telemetry, now).roster?.map(player => player.ping)).toEqual([null, null]);
});
it('gives GUID matches priority over conflicting names and never reuses them', () => {
const telemetry = report(0, 2);
const raw = server({ roster: [
{ num: 0, score: 100, ping: 42, guid: telemetry.players[1].guid, name: telemetry.players[0].name },
{ num: 1, score: 100, ping: 99, guid: 'another-guid', name: telemetry.players[1].name }
] });
expect(selectServerObservation(server({ rcon_status: raw }), telemetry, now).roster?.map(player => player.ping)).toEqual([null, 42]);
});
it('uses the latest HTTP or streamed telemetry and falls back without extending freshness', () => {
const original = server();
const selected = selectServerObservation(server({ rcon_status: original, telemetry: report(0, 2) }), report(-1000), now);
+3 -2
View File
@@ -501,7 +501,8 @@ describe('database fleet and container readiness', () => {
rcon.mockResolvedValue({ ok: true, endpoint: { host: 'localhost', port: 28960 }, rawReply: 'status',
status: { players: 4, map: 'zombie_theater', gametype: 'zom', roster: [
{ num: 0, guid: telemetry.players[0].guid, name: 'Old name', score: 50, ping: 100 },
{ num: 1, guid: telemetry.players[1].guid, name: 'Another old name', score: 100, ping: 30 }
{ num: 1, guid: telemetry.players[1].guid, name: 'Another old name', score: 100, ping: 30 },
{ num: 2, guid: '6C45DD', name: telemetry.players[2].name, score: 150, ping: 151 }
] } });
await postTelemetry(telemetry);
const events: unknown[] = [];
@@ -514,7 +515,7 @@ describe('database fleet and container readiness', () => {
expect(server.roster).toHaveLength(3);
expect(server.roster[0]).toMatchObject({ ping: 42, score: 5000 });
expect(server.roster[1]).toMatchObject({ ping: 30, score: 5000, name: 'Player 2' });
expect(server.roster[2].ping).toBeNull();
expect(server.roster[2]).toMatchObject({ ping: 151, guid: telemetry.players[2].guid, name: 'Player 3', score: 5000 });
expect(events).toContainEqual(expect.objectContaining({ type: 'server-status', entries: [expect.objectContaining({
health: 'online', players: 3, roster: server.roster, lastSeenAt: server.last_seen_at
})] }));