ADR: Save/Load-Format und Migration #7

Closed
opened 2026-08-10 07:58:34 +00:00 by RDietermann · 1 comment
Owner

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".

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".
RDietermann added the wayfinder:task label 2026-08-10 07:58:34 +00:00
Author
Owner

Entschieden. ADR in docs/adr/0003-save-format-ort-und-migration.md.

Der Meta-Fortschritt liegt als TOML in einer eigenen progress.toml im plattformüblichen Preferences-Verzeichnis, geschrieben über Bevys eingebautes bevy_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. version ist 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.md hat 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 version nach dem Laden auf 0, darf die nächste Aktion des Programms kein save sein, 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

  • Beschädigte Datei — Parse-Fehler, alles bleibt Default, version == 0, Meldung plus Schreibsperre.
  • Halb geschriebene Datei nach Absturz — kann progress.toml nicht erreichen; geschrieben wird progress.toml.new, erst das Umbenennen macht sie gültig. Der Spieler merkt nichts, der alte Stand lädt.
  • Save aus neuerer Version — Schreiben gesperrt, Datei unangetastet, Meldung.
  • Fehlende Schreibrechte — die App erfährt es nicht, es gibt in 0.19 keinen Rückkanal. Konsequenz: keine „Gespeichert"-Anzeige, weil das Programm sie nicht belegen könnte. Als offene Lücke benannt, nicht kaschiert.

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.0 und docs.rs geprüft — Modulpfad und Funktionsname je Behauptung. Zwei Ergebnisse verdienen Erwähnung:

Ein Datenverlust-Pfad in bevy_settings ist durch Code-Lesen belegt: in store_fs.rs fehlt sowohl in save als auch in save_async das return nach einem fehlgeschlagenen fs::write — der Fehler wird geloggt, und das fs::rename lä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: ein Error saving settings file-Log oder eine liegengebliebene progress.toml.new.

Eine eigene frühere Aussage wurde richtiggestellt: Lese- und Parse-Fehler werden als error! geloggt, nicht als warn!; 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 Resource vs. Component hält nur einseitig: persistiert ⊆ Resource, aber nicht umgekehrt — Balance ist der Gegenbeleg. Und es gibt genau eine benannte Ausnahme: highest_wave und 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 Fassade toml. Unterbau, serde, thiserror, web-sys und windows-sys liegen bereits drin.

**Entschieden.** ADR in `docs/adr/0003-save-format-ort-und-migration.md`. Der Meta-Fortschritt liegt als **TOML** in einer eigenen `progress.toml` im plattformüblichen Preferences-Verzeichnis, geschrieben über Bevys eingebautes **`bevy_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. `version` ist 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.md` hat 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 `version` nach dem Laden auf 0, darf die nächste Aktion des Programms kein `save` sein, 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 - **Beschädigte Datei** — Parse-Fehler, alles bleibt Default, `version == 0`, Meldung plus Schreibsperre. - **Halb geschriebene Datei nach Absturz** — kann `progress.toml` nicht erreichen; geschrieben wird `progress.toml.new`, erst das Umbenennen macht sie gültig. Der Spieler merkt nichts, der alte Stand lädt. - **Save aus neuerer Version** — Schreiben gesperrt, Datei unangetastet, Meldung. - **Fehlende Schreibrechte** — die App erfährt es nicht, es gibt in 0.19 keinen Rückkanal. Konsequenz: **keine „Gespeichert"-Anzeige**, weil das Programm sie nicht belegen könnte. Als offene Lücke benannt, nicht kaschiert. ## 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.0` und docs.rs geprüft — Modulpfad und Funktionsname je Behauptung. Zwei Ergebnisse verdienen Erwähnung: **Ein Datenverlust-Pfad in `bevy_settings` ist durch Code-Lesen belegt**: in `store_fs.rs` fehlt sowohl in `save` als auch in `save_async` das `return` nach einem fehlgeschlagenen `fs::write` — der Fehler wird geloggt, und das `fs::rename` lä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: ein `Error saving settings file`-Log oder eine liegengebliebene `progress.toml.new`. **Eine eigene frühere Aussage wurde richtiggestellt**: Lese- und Parse-Fehler werden als `error!` geloggt, nicht als `warn!`; `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 `Resource` vs. `Component`* hält nur **einseitig**: persistiert ⊆ `Resource`, aber nicht umgekehrt — `Balance` ist der Gegenbeleg. Und es gibt genau eine benannte Ausnahme: `highest_wave` und 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 Fassade `toml`. Unterbau, `serde`, `thiserror`, `web-sys` und `windows-sys` liegen bereits drin.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RDietermann/InfiniTower#7