diff --git a/docs/ls-feed.md b/docs/ls-feed.md index eced3eb..05a0b18 100644 --- a/docs/ls-feed.md +++ b/docs/ls-feed.md @@ -90,11 +90,27 @@ Autosave geschrieben und hinkt bis zu einer Viertelstunde hinterher. Über zwei Stunden gemessen stieg `playTime` um 50, während die Spieluhr 18,7 Stunden weiterlief — daraus lässt sich nichts rechnen. -`dayTime` in der Statusabfrage ist dagegen live und zählt Millisekunden seit -Tagesbeginn. Bei `plannedDaysPerPeriod = 1` ist jeder Rücksprung um Mitternacht -ein Monatswechsel. Also: Monat einmal im Panel eintragen, danach zählt der Bot -an dieser Uhr selbst weiter. Steht der Server leer, steht auch die Spielzeit — -und der Monat bleibt richtigerweise stehen. +`dayTime` in der Statusabfrage zählt Millisekunden seit Tagesbeginn und ist +die brauchbarste Uhr im Feed — aber **auch nur ein Schnappschuss**. Der Server +schreibt das XML im „Web API Interval" neu, dazwischen steht derselbe Wert. +Gemessen: fünf Abrufe über zwei Minuten, ein Spieler online, alle identisch +(`dayTime=37028620`). + +Bei `plannedDaysPerPeriod = 1` ist jeder Rücksprung um Mitternacht ein +Monatswechsel. Also: Monat einmal im Panel eintragen, danach zählt der Bot an +dieser Uhr selbst weiter. Steht der Server leer, steht auch die Spielzeit — und +der Monat bleibt richtigerweise stehen. + +**Ein Rücksprung ist nicht automatisch Mitternacht.** Wer einfach auf +`jetzt < vorher` prüft, ruft bei jeder kaputten Antwort einen neuen Monat aus: +eine fehlende `dayTime` wird schnell zu `0`, und `0` ist kleiner als jeder +Vortagswert. Ein echter Wechsel geht vom späten Abend in den frühen Morgen — +also beide Werte prüfen, nicht nur ihre Differenz. + +Bleibt ein Restrisiko: ein Serverneustart lädt den Spielstand und setzt die Uhr +auf den gespeicherten Zeitpunkt zurück. Fällt der zufällig in den frühen +Morgen, während vorher später Abend gemessen wurde, zählt der Bot einen Monat +zu viel. Dann im Panel den richtigen eintragen — von da an läuft es weiter. Die zwölf Perioden sind übrigens schlicht Monate: `EARLY_SPRING` ist März, `EARLY_AUTUMN` September. diff --git a/src/bot/ls-farm.js b/src/bot/ls-farm.js index cc54576..0aa05a2 100644 --- a/src/bot/ls-farm.js +++ b/src/bot/ls-farm.js @@ -21,6 +21,13 @@ import { getSetting, setSetting, listGameservers, lsState, setLsMessage, setLsBe const attrsVon = (roh) => Object.fromEntries([...roh.matchAll(/(\w+)="([^"]*)"/g)].map((m) => [m[1], m[2]])); +/** Zahl oder null — leer und Unsinn ergeben nicht 0, sondern „weiss ich nicht" */ +function zahlOderNull(roh) { + if (roh == null || String(roh).trim() === '') return null; + const n = Number(roh); + return Number.isFinite(n) ? n : null; +} + /** Alle selbstschliessenden Tags eines Namens */ function tags(xml, name) { return [...xml.matchAll(new RegExp(`<${name} ([^>]*?)/>`, 'g'))].map((m) => attrsVon(m[1])); @@ -44,10 +51,15 @@ export function statsLesen(xml) { // Ohne mapSize lässt sich nichts zeichnen — dann lieber gar keine Karte groesse: Number(server.mapSize) || null, version: server.version ?? null, - // Millisekunden seit Tagesbeginn. Die einzige Uhr im Feed, die wirklich - // live ist — playTime aus dem Savegame wird nur beim Autosave neu - // geschrieben und hinkt bis zu einer Viertelstunde hinterher. - tageszeit: Number(server.dayTime) || 0, + // Millisekunden seit Tagesbeginn — die brauchbarste Uhr im Feed, aber + // auch nur ein Schnappschuss: der Server schreibt das XML im „Web API + // Interval" neu, dazwischen steht derselbe Wert. Gemessen: fünf Proben + // über zwei Minuten mit einem Spieler online, alle identisch. + // + // `null` statt `0`, wenn nichts dasteht. Eine fehlende Angabe ist keine + // Mitternacht — mit `|| 0` sah jede kaputte Antwort wie ein + // Tageswechsel aus und stellte den Monat weiter. + tageszeit: zahlOderNull(server.dayTime), parzellen: tags(xml, 'Farmland').map((f) => ({ id: Number(f.id), name: f.name ?? String(f.id), hof: Number(f.owner), // 0 = noch zu kaufen @@ -591,7 +603,10 @@ export async function lsTick(client) { }).catch(() => {}); } } - setLsTageszeit(s.id, stand.tageszeit); + // Eine Antwort ohne Uhrzeit darf den letzten guten Stand nicht + // überschreiben — sonst gilt beim nächsten Durchlauf „nichts + // bekannt", und der echte Mitternachtssprung geht verloren. + if (stand.tageszeit != null) setLsTageszeit(s.id, stand.tageszeit); if (!kanal?.isTextBased()) continue; const payload = await hofNachricht(client, stand, bild, namen, breite); @@ -632,8 +647,23 @@ export async function lsTick(client) { * Bewusst über die Uhr und nicht über die Wanduhr: steht der Server leer, * steht auch die Spielzeit, und dann darf der Monat nicht weiterlaufen. */ -export const tagUmgeschlagen = (vorher, jetzt) => - Number.isFinite(vorher) && vorher > 0 && jetzt < vorher; +const TAG_MS = 24 * 60 * 60 * 1000; +// Ein echter Mitternachtssprung geht vom späten Abend in den frühen Morgen. +// Die Schwellen sind grosszügig, damit auch ein seltener Abruf ihn erwischt, +// und trotzdem eng genug, dass ein Zappeln um ein paar Minuten durchfällt. +const SPAET = TAG_MS * 0.6; +const FRUEH = TAG_MS * 0.4; + +export function tagUmgeschlagen(vorher, jetzt) { + // Fehlt eine der beiden Angaben, wissen wir es nicht — und „weiss nicht" + // ist kein Monatswechsel. Genau hier lag der Fehler: eine Antwort ohne + // dayTime wurde zu 0, und 0 ist kleiner als jeder Vortagswert. + if (!Number.isFinite(vorher) || !Number.isFinite(jetzt)) return false; + if (vorher <= 0 || jetzt <= 0) return false; + // Werte ausserhalb eines Tages sind Unsinn und keine Grundlage + if (vorher > TAG_MS || jetzt > TAG_MS) return false; + return vorher > SPAET && jetzt < FRUEH; +} /** Nächste Jahreszeit; unbekannte Eingabe ergibt null statt eines Ratewerts */ export function naechstePeriode(periode) {