Kein Monatswechsel mehr aus einer kaputten Antwort
Der Bot rief einen neuen Monat aus, obwohl im Spiel keiner war. Zwei Fehler, die zusammen genau das ergeben: statsLesen las die Uhrzeit als `Number(server.dayTime) || 0`. Eine Antwort ohne dayTime — Server startet neu, Proxy schiebt eine Fehlerseite dazwischen, XML halb geschrieben — wurde damit zu 0. Und tagUmgeschlagen prueft auf `jetzt < vorher`. Null ist kleiner als jeder Vortagswert, also sah jede kaputte Antwort wie Mitternacht aus. Beides behoben: fehlt die Angabe, ist sie jetzt null und nicht 0 — "weiss nicht" ist kein Tageswechsel. Und ein Wechsel muss die Form eines echten Mitternachtssprungs haben: vom spaeten Abend in den fruehen Morgen, beide Werte innerhalb eines Tages. Ein Zappeln um ein paar Minuten faellt jetzt durch. Eine Antwort ohne Uhrzeit ueberschreibt ausserdem den letzten guten Stand nicht mehr, sonst ginge der echte Sprung danach verloren. Beim Nachmessen zeigte sich, dass meine Doku an der Stelle falsch war: dayTime ist nicht live, sondern ein Schnappschuss. Fuenf Abrufe ueber zwei Minuten mit einem Spieler online ergaben denselben Wert (37028620) — der Server schreibt das XML im "Web API Interval" neu, hier alle 360 Sekunden. Steht jetzt richtig in docs/ls-feed.md, samt dem verbleibenden Restrisiko: ein Serverneustart setzt die Uhr auf den gespeicherten Stand zurueck und kann in seltenen Faellen einen Monat zu viel zaehlen. Geprueft: echter Sprung, normaler Tagesverlauf, Erstlauf, fehlende Angabe als 0/null/NaN, kleiner Ruecksprung, Ruecksprung am Vormittag, unsinnige Groessen, und dass eine fehlende dayTime im XML als null ankommt statt als 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+21
-5
@@ -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
|
Stunden gemessen stieg `playTime` um 50, während die Spieluhr 18,7 Stunden
|
||||||
weiterlief — daraus lässt sich nichts rechnen.
|
weiterlief — daraus lässt sich nichts rechnen.
|
||||||
|
|
||||||
`dayTime` in der Statusabfrage ist dagegen live und zählt Millisekunden seit
|
`dayTime` in der Statusabfrage zählt Millisekunden seit Tagesbeginn und ist
|
||||||
Tagesbeginn. Bei `plannedDaysPerPeriod = 1` ist jeder Rücksprung um Mitternacht
|
die brauchbarste Uhr im Feed — aber **auch nur ein Schnappschuss**. Der Server
|
||||||
ein Monatswechsel. Also: Monat einmal im Panel eintragen, danach zählt der Bot
|
schreibt das XML im „Web API Interval" neu, dazwischen steht derselbe Wert.
|
||||||
an dieser Uhr selbst weiter. Steht der Server leer, steht auch die Spielzeit —
|
Gemessen: fünf Abrufe über zwei Minuten, ein Spieler online, alle identisch
|
||||||
und der Monat bleibt richtigerweise stehen.
|
(`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,
|
Die zwölf Perioden sind übrigens schlicht Monate: `EARLY_SPRING` ist März,
|
||||||
`EARLY_AUTUMN` September.
|
`EARLY_AUTUMN` September.
|
||||||
|
|||||||
+37
-7
@@ -21,6 +21,13 @@ import { getSetting, setSetting, listGameservers, lsState, setLsMessage, setLsBe
|
|||||||
const attrsVon = (roh) =>
|
const attrsVon = (roh) =>
|
||||||
Object.fromEntries([...roh.matchAll(/(\w+)="([^"]*)"/g)].map((m) => [m[1], m[2]]));
|
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 */
|
/** Alle selbstschliessenden Tags eines Namens */
|
||||||
function tags(xml, name) {
|
function tags(xml, name) {
|
||||||
return [...xml.matchAll(new RegExp(`<${name} ([^>]*?)/>`, 'g'))].map((m) => attrsVon(m[1]));
|
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
|
// Ohne mapSize lässt sich nichts zeichnen — dann lieber gar keine Karte
|
||||||
groesse: Number(server.mapSize) || null,
|
groesse: Number(server.mapSize) || null,
|
||||||
version: server.version ?? null,
|
version: server.version ?? null,
|
||||||
// Millisekunden seit Tagesbeginn. Die einzige Uhr im Feed, die wirklich
|
// Millisekunden seit Tagesbeginn — die brauchbarste Uhr im Feed, aber
|
||||||
// live ist — playTime aus dem Savegame wird nur beim Autosave neu
|
// auch nur ein Schnappschuss: der Server schreibt das XML im „Web API
|
||||||
// geschrieben und hinkt bis zu einer Viertelstunde hinterher.
|
// Interval" neu, dazwischen steht derselbe Wert. Gemessen: fünf Proben
|
||||||
tageszeit: Number(server.dayTime) || 0,
|
// ü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) => ({
|
parzellen: tags(xml, 'Farmland').map((f) => ({
|
||||||
id: Number(f.id), name: f.name ?? String(f.id),
|
id: Number(f.id), name: f.name ?? String(f.id),
|
||||||
hof: Number(f.owner), // 0 = noch zu kaufen
|
hof: Number(f.owner), // 0 = noch zu kaufen
|
||||||
@@ -591,7 +603,10 @@ export async function lsTick(client) {
|
|||||||
}).catch(() => {});
|
}).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;
|
if (!kanal?.isTextBased()) continue;
|
||||||
const payload = await hofNachricht(client, stand, bild, namen, breite);
|
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,
|
* Bewusst über die Uhr und nicht über die Wanduhr: steht der Server leer,
|
||||||
* steht auch die Spielzeit, und dann darf der Monat nicht weiterlaufen.
|
* steht auch die Spielzeit, und dann darf der Monat nicht weiterlaufen.
|
||||||
*/
|
*/
|
||||||
export const tagUmgeschlagen = (vorher, jetzt) =>
|
const TAG_MS = 24 * 60 * 60 * 1000;
|
||||||
Number.isFinite(vorher) && vorher > 0 && jetzt < vorher;
|
// 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 */
|
/** Nächste Jahreszeit; unbekannte Eingabe ergibt null statt eines Ratewerts */
|
||||||
export function naechstePeriode(periode) {
|
export function naechstePeriode(periode) {
|
||||||
|
|||||||
Reference in New Issue
Block a user