ADR: Save/Load-Format und Migration #7
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #1
Blocked by: #6
Entscheidung plus ADR unter
docs/adr/.Die Frage: Was wird persistiert (Meta-Fortschritt: Workshop-Level, freigeschaltete Cards, Währung — ein laufender Run ausdrücklich nicht), in welchem Format, an welchem Ort, und wie überlebt ein Save eine Feld-Änderung?
Randbedingungen: kein Cloud-Save, kein Account. Der Speicherort muss plattformüblich sein, ohne dass der Spielkern eine Plattform-API sieht.
Fertig, wenn die ADR Format, Pfad-Strategie und Migrations-Regel benennt — inklusive der Antwort auf „was passiert bei einem kaputten Save".
Entschieden. ADR in
docs/adr/0003-save-format-ort-und-migration.md.Der Meta-Fortschritt liegt als TOML in einer eigenen
progress.tomlim plattformüblichen Preferences-Verzeichnis, geschrieben über Bevys eingebautesbevy_settings(nicht-default Cargo-Feature). Migration läuft über Feldnamen als Vertrag: Hinzufügen und Entfernen sind gratis, eine Bedeutungsänderung erzwingt einen neuen Schlüsselnamen, nie die Neuinterpretation des alten.versionist nur die Notausfahrt, nicht der Regelweg.Persistierte Felder
version: u32(Sentinel,Default = 0, beim Schreiben immer ≥ 1) ·workshop_levels(Struct mit benanntem Feld je Upgrade, nicht positionsbasiert) ·card_levels(benanntes Feld je Card,0= nicht besessen) ·equipped_cards(Liste von Namen, nicht Indizes) ·card_slots: u8· die Meta-Währung, ganzzahlig ·highest_wave: u32.Kein Zeitstempel — ohne Offline-Progress hat er keinen Leser. Kein laufender Run, kein
Balance.Der Feldname der Meta-Währung ist bewusst offen gelassen:
CONTEXT.mdhat für diese Ressource noch keinen Begriff, und ein vorläufiger Name wäre nach der Migrationsregel oben ein Fehler, den man nicht mehr billig korrigiert.Drei Regeln, die unabhängig von der Framework-Wahl gelten
Niemals schreiben, bevor bestätigt gelesen wurde. Steht
versionnach dem Laden auf 0, darf die nächste Aktion des Programms keinsavesein, sondern eine Aussage an den Spieler.Der Sentinel unterscheidet drei Wege:
0→ nicht gelesen, Meldung und Schreibsperre bis zur ersten bewussten Aktion ·== CURRENT→ normal ·> CURRENT→ Save aus neuerer Version, alle Schreibvorgänge für die Sitzung gesperrt, Datei unangetastet.Bedeutungsänderung = neuer Schlüsselname. Das ist der eine Migrationsfall, den Feld-Toleranz prinzipbedingt nicht abfängt: bleibt der Name gleich und ändert sich die Semantik, wendet der Loader den alten Wert widerspruchslos falsch an.
Die vier Fehlerfälle
version == 0, Meldung plus Schreibsperre.progress.tomlnicht erreichen; geschrieben wirdprogress.toml.new, erst das Umbenennen macht sie gültig. Der Spieler merkt nichts, der alte Stand lädt.Belegtiefe
Die ADR hat einen eigenen Abschnitt dazu, weil die Entscheidung auf Behauptungen über fremden Code steht. Elf Punkte wurden gegen das Bevy-Repo am Tag
v0.19.0und docs.rs geprüft — Modulpfad und Funktionsname je Behauptung. Zwei Ergebnisse verdienen Erwähnung:Ein Datenverlust-Pfad in
bevy_settingsist durch Code-Lesen belegt: instore_fs.rsfehlt sowohl insaveals auch insave_asyncdasreturnnach einem fehlgeschlagenenfs::write— der Fehler wird geloggt, und dasfs::renameläuft trotzdem und schiebt die abgeschnittene Datei über den intakten Save. Atomares Schreiben schützt damit gegen Absturz, nicht gegen volle Platte. Kein realer Datenverlust beobachtet, Upstream-Tracker nicht geprüft; steht so in der ADR und ist Revisions-Auslöser Nummer 1, mit zwei beobachtbaren Belegen: einError saving settings file-Log oder eine liegengebliebeneprogress.toml.new.Eine eigene frühere Aussage wurde richtiggestellt: Lese- und Parse-Fehler werden als
error!geloggt, nicht alswarn!;warn!gilt nur der fehlenden Datei. Und eine Annahme wurde ausdrücklich nicht geprüft und deshalb auch nicht getroffen — dass eine abgeschnittene TOML-Datei immer ungültig sei. Sie ist es nicht zwangsläufig: eine Datei, die nach einer vollständigen Zeile endet, parst tadellos und setzt den Rest still auf Default. Genau deshalb trägt der Sentinel und nicht der Parser.Ein Befund zur Ablageregel aus ADR-0002
Die Regel Lebensdauer bestimmt
Resourcevs.Componenthält nur einseitig: persistiert ⊆Resource, aber nicht umgekehrt —Balanceist der Gegenbeleg. Und es gibt genau eine benannte Ausnahme:highest_waveund die im Run verdiente Meta-Währung überqueren die Run-Grenze, und zwar genau einmal am Run-Ende in einem einzigen System.Dependencies: keine deklarierte. Nötig ist das Feature
bevy_settings; transitiv kommt genau eine Kiste neu in den Lock — die Fassadetoml. Unterbau,serde,thiserror,web-sysundwindows-sysliegen bereits drin.