diff --git a/src/bot/server-monitor.js b/src/bot/server-monitor.js index 728da0c..14bc6ba 100644 --- a/src/bot/server-monitor.js +++ b/src/bot/server-monitor.js @@ -18,7 +18,7 @@ const RED = 0xf23f43; // standen mal ausgedachte wie "arkse" oder "terraria", die es dort gar nicht // gibt. Beim Ändern gegen `games` aus gamedig prüfen, nicht gegen das Gefühl. export const GAME_ICONS = { - fivem: '🚗', http: '🌐', minecraft: '⛏️', mbe: '⛏️', + fivem: '🚗', http: '🌐', minecraft: '⛏️', mbe: '⛏️', panel: '🛰️', teamfortress2: '🎩', counterstrike2: '🔫', csgo: '🔫', garrysmod: '🔧', rust: '🪓', valheim: '🛡️', palworld: '🐑', ase: '🦖', asa: '🦖', pixark: '🦖', dayz: '🧟', projectzomboid: '🧟', sdtd: '🧟', @@ -27,9 +27,13 @@ export const GAME_ICONS = { }; // fivem und http sind unsere eigenen Typen — gamedig kennt sie nicht. +// `panel` ist der dritte, und er ist anders als die beiden: da fragt der Bot +// gar nicht, sondern das Panel hat schon gefragt und schickt die Zahl mit. +// Siehe `queryServer`. const EIGENE = { fivem: { name: 'FiveM', port: 30120 }, http: { name: 'HTTP-Check', port: null }, + panel: { name: 'Über das Panel', port: null }, }; // Protokolle, die ohne Zugangs-Code gar nicht antworten. Der Code steht im @@ -73,8 +77,15 @@ const alertState = new Map(); // id → { fails, down, since } // Letztes Query-Ergebnis pro Server — für die öffentliche /server-Seite export const lastResults = new Map(); // id → Query-Ergebnis + checkedAt -/** Einen Server abfragen — fivem/http direkt, alles andere über gamedig */ -export async function queryServer(server) { +/** + * Einen Server abfragen — fivem/http direkt, `panel` gar nicht, alles andere + * über gamedig. + * + * `panelItems` ist der Statusbericht des Panels aus DIESEM Durchlauf. Er ist + * optional: fehlt er, verhält sich alles wie vorher, und ein `panel`-Eintrag + * gilt als offline — mit Grund. + */ +export async function queryServer(server, panelItems = null) { const base = { ...server, online: false, players: null, max: null, map: null, playerNames: [], ping: null, version: null, mods: null, @@ -84,6 +95,34 @@ export async function queryServer(server) { }; const started = Date.now(); try { + // ================== DAS PANEL HAT SCHON GEFRAGT ==================== + // + // Kein Netzverkehr von hier. Für TeamSpeak wäre er sogar unmöglich: + // gamedig braucht dafür den ServerQuery-Port, und der liegt beim + // Betreiber absichtlich auf `127.0.0.1` — der Bot im Container kommt + // dort nie hin. Das Panel läuft auf dem Host, darf hin und hat die + // Zugangsdaten. Also fragt es, und wir lesen ab. + // + // `spieler === null` HEISST „KEINE ANTWORT", NICHT NULL SPIELER. Das + // Panel garantiert dazu einen `spieler_grund` — entweder eine Zahl + // oder ein Grund, nie beides leer. Genau diese Zusage macht das hier + // sauber: wir werfen mit dem Grund und landen im selben Zweig wie ein + // gescheiterter gamedig-Query, statt eine 0 zu erfinden. + if (server.type === 'panel') { + const p = panelEintrag(panelItems, server); + if (!p) throw new Error('Das Panel kennt diesen Server nicht (mehr).'); + if (p.spieler == null) throw new Error(p.spieler_grund || 'Das Panel nennt keine Zahl.'); + return { + ...base, online: true, + players: p.spieler, + max: p.spieler_max ?? null, + spielname: p.spiel ?? null, + // KEIN PING. Die Zahl ist eine gespeicherte Messung des Panels + // (bis zu zehn Minuten alt) und keine Antwortzeit von hier. + // Eine Millisekundenzahl hinzuschreiben wäre erfunden. + ping: null, + }; + } if (server.type === 'fivem') { const res = await fetch(`${server.query_url.replace(/\/$/, '')}/dynamic.json`, { signal: AbortSignal.timeout(tuningMs.seconds('monitor_timeout')), @@ -406,7 +445,13 @@ export async function monitorTick(client) { const servers = listGameservers(); if (servers.length === 0) return; - const results = await Promise.all(servers.map(queryServer)); + // NICHT `servers.map(queryServer)` -- so stand es hier, und mit dem + // zweiten Parameter wäre es eine Falle gewesen: `map` reicht (element, + // index, array) weiter, `panelItems` bekäme also die Zahl 0. Der erste + // Server hätte damit `null` gesehen und alle weiteren eine Zahl, an der + // `panelEintrag` still scheitert -- ein Fehler, der nur beim zweiten + // Eintrag auftritt und nach Zufall aussieht. + const results = await Promise.all(servers.map((s) => queryServer(s, panelItems))); // Die Zusatzauskunft vom Panel: EINMAL je Durchlauf, nicht je Server. // Sie ist optional -- ist nichts eingetragen oder antwortet das Panel diff --git a/src/panel-abgleich.js b/src/panel-abgleich.js index 8d9979e..4287ce2 100644 --- a/src/panel-abgleich.js +++ b/src/panel-abgleich.js @@ -33,9 +33,28 @@ const uebernehmenStmt = db.prepare('UPDATE gameservers SET panel_name = ? WHERE * sie noch einmal, weil ein Bericht von einem älteren Panel kommen kann und * ein halber Eintrag schlimmer ist als keiner: er stünde dauerhaft auf * offline und schlüge Alarm. + * + * ================== WARUM NICHT MEHR `x.gamedig` ========================== + * + * Hier stand `x.gamedig`, also: „kennt unsere Bibliothek dieses Spiel?" Das + * hat einen Fall ausgesperrt, den es wirklich gibt — TeamSpeak. Nicht weil + * gamedig es nicht könnte (es hat `teamspeak3`), sondern weil der + * ServerQuery-Port des Betreibers absichtlich auf `127.0.0.1` liegt: der Bot + * läuft im Container und kommt dort nie hin. Am 16.08.2026 gegengeprüft, + * beide Versuche vom Host aus gescheitert. + * + * Das Panel dagegen sitzt auf dem Host, darf dorthin und hat die Zugangsdaten + * — es schickt seine eigene Messung seither im Statusbericht mit. Gefragt + * wird deshalb `zeigbar`: „kann ÜBERHAUPT JEMAND diesen Server abfragen — + * wir über gamedig oder das Panel für uns?" + * + * `?? Boolean(x.gamedig)` ist der Rückfall für ein ÄLTERES Panel, das + * `zeigbar` noch nicht kennt. Ohne ihn verschwänden nach einem Bot-Update + * schlagartig alle Einträge, und zwar lautlos. */ export function taugt(x) { - return Boolean(x?.discord && x.gamedig && x.adresse && x.port); + const zeigbar = x?.zeigbar ?? Boolean(x?.gamedig); + return Boolean(x?.discord && zeigbar && x.adresse && x.port); } /** @@ -86,7 +105,12 @@ export function abgleichen(items) { createGameserver({ name: x.name, - type: x.gamedig, + // OHNE gamedig-KENNUNG FRAGEN WIR DAS PANEL. Der Typ `panel` ist + // unser dritter eigener neben `fivem` und `http` — siehe + // `queryServer`. Wo gamedig kann, bleibt es bei gamedig: dessen + // Antwort bringt Karte, Fassung und Ping mit, die das Panel nicht + // liefert. Der neue Typ schliesst eine Luecke, er ersetzt nichts. + type: x.gamedig || 'panel', host: x.adresse, port: x.port, address: `${x.adresse}:${x.port}`, diff --git a/tools/panel-abgleich-pruefen.mjs b/tools/panel-abgleich-pruefen.mjs index c4c4707..2a99a9d 100644 --- a/tools/panel-abgleich-pruefen.mjs +++ b/tools/panel-abgleich-pruefen.mjs @@ -49,6 +49,42 @@ zeig(taugt(panel({ adresse: '' })) === false, 'ohne Adresse für Spieler -> nein zeig(taugt(panel({ port: null })) === false, 'ohne Port -> nein'); zeig(taugt(undefined) === false, 'gar nichts -> nein'); +console.log(''); +console.log('=== Was das Panel selbst abfragen kann (seit 16.08.2026)'); +// +// DER FALL, UM DEN ES GEHT: TeamSpeak. gamedig KANN es -- das Protokoll +// `teamspeak3` gibt es, DiscordGSM fuehrt es ebenfalls. Es braucht dafuer +// aber den ServerQuery-Port, und der liegt beim Betreiber absichtlich auf +// 127.0.0.1 (gemessen: 10011/tcp -> 127.0.0.1:9988). Der Bot im Container +// kommt dort nie hin, das Panel auf dem Host schon -- und es hat die +// Zugangsdaten. Also sagt das Panel `zeigbar: true` OHNE gamedig-Kennung, +// und wir lesen seine Zahl ab, statt einen Verwaltungsport zu oeffnen. +zeig(taugt(panel({ gamedig: null, zeigbar: true })) === true, 'ohne gamedig, aber das Panel kann fragen -> ja'); +zeig(taugt(panel({ gamedig: 'minecraft', zeigbar: false })) === false, 'Panel sagt nein -> nein, auch mit gamedig-Kennung'); +// DER RUECKFALL FUER EIN AELTERES PANEL. Ohne ihn verschwaenden nach einem +// Bot-Update schlagartig alle Eintraege, und zwar lautlos. +zeig(taugt(panel({ gamedig: 'minecraft' })) === true, 'aelteres Panel ohne `zeigbar` -> weiterhin ja'); +zeig(taugt(panel({ gamedig: null })) === false, 'aelteres Panel, kein gamedig -> weiterhin nein'); + +console.log(''); +console.log('=== Und welcher Typ dabei entsteht'); +{ + const vorher = db.listGameservers().length; + const b = abgleichen([panel({ name: 'ts3', gamedig: null, zeigbar: true, port: 9987 })]); + zeig(b.angelegt.includes('ts3'), 'ein Server ohne gamedig-Kennung wird angelegt', b.angelegt.join()); + const t = db.listGameservers().find((s) => s.name === 'ts3'); + zeig(t?.type === 'panel', 'und zwar mit dem Typ `panel` -- da fragt der Bot nicht selbst', t?.type); + // Wo gamedig kann, bleibt es bei gamedig: dessen Antwort bringt Karte, + // Fassung und Ping mit, die das Panel nicht liefert. + abgleichen([panel({ name: 'ts3', gamedig: null, zeigbar: true, port: 9987 }), panel()]); + zeig(db.listGameservers().find((s) => s.name === 'p1')?.type === 'minecraft', 'mit Kennung bleibt es bei gamedig -- der neue Typ ersetzt nichts'); + // Aufraeumen, damit die folgenden Abschnitte auf dem Stand von vorher + // rechnen. Eine Probe, die den Stand veraendert, macht die naechste Zeile + // unlesbar. + abgleichen([]); + zeig(db.listGameservers().length === vorher, 'und danach ist wieder aufgeraeumt'); +} + console.log(''); console.log('=== Anlegen'); let a = abgleichen([panel()]); @@ -111,6 +147,39 @@ zeig(danach.image_url === 'https://neu', 'und die Änderung ist drin'); abgleichen([panel()]); zeig(db.getGameserver(p1.id).image_url === 'https://neu', 'der nächste Durchlauf plättet sie nicht'); +console.log(''); +console.log('=== Der Typ `panel`: was der Bot daraus macht'); +{ + const { queryServer } = await import('../src/bot/server-monitor.js'); + const eintrag = { id: 99, name: 'ts3', type: 'panel', host: '1.2.3.4', port: 9987 }; + const bericht = (o) => [{ + name: 'ts3', port: 9987, spiel: 'TeamSpeak 3', zeigbar: true, gamedig: null, + spieler: 3, spieler_max: 32, spieler_grund: null, ...o, + }]; + + const r = await queryServer(eintrag, bericht()); + zeig(r.online === true && r.players === 3 && r.max === 32, 'die Zahl des Panels wird uebernommen', `${r.players}/${r.max}`); + zeig(r.spielname === 'TeamSpeak 3', 'samt Spielname aus dem Bericht', r.spielname); + // KEIN ERFUNDENER PING. Die Zahl ist eine gespeicherte Messung des Panels + // und keine Antwortzeit von hier. + zeig(r.ping === null, 'aber KEIN Ping -- den haben wir nicht gemessen', String(r.ping)); + + // null HEISST KEINE ANTWORT, NICHT NULL SPIELER. Das ist der ganze Punkt: + // ein leerer Server und ein unerreichbarer duerfen nicht gleich aussehen. + const ohne = await queryServer(eintrag, bericht({ spieler: null, spieler_grund: 'Der Server läuft nicht.' })); + zeig(ohne.online === false, 'ohne Zahl: offline, statt eine 0 zu erfinden'); + zeig(ohne.players === null, 'und die Spielerzahl bleibt null', String(ohne.players)); + + // NULL SPIELER SIND EINE ANTWORT. Der haeufigste Zustand ueberhaupt -- + // wer ihn als "offline" liest, faerbt einen laufenden Server rot. + const leer = await queryServer(eintrag, bericht({ spieler: 0 })); + zeig(leer.online === true && leer.players === 0, 'null Spieler sind eine Antwort, kein Ausfall'); + + // Und die Faelle, in denen gar nichts da ist. + zeig((await queryServer(eintrag, null)).online === false, 'ohne Panel-Bericht: offline statt Absturz'); + zeig((await queryServer(eintrag, [])).online === false, 'leerer Bericht: offline statt Absturz'); +} + console.log(''); console.log('=== Kaputte Berichte'); zeig(abgleichen(null).angelegt.length === 0, 'null -> nichts passiert');