docs: add ADR-0003 on save format, location and migration
This commit is contained in:
304
docs/adr/0003-save-format-ort-und-migration.md
Normal file
304
docs/adr/0003-save-format-ort-und-migration.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# ADR-0003: Save-Format, Speicherort und Migration
|
||||
|
||||
- **Status:** Angenommen
|
||||
- **Datum:** 2026-08-10
|
||||
- **Betrifft:** Persistenz des Meta-Fortschritts (Workshop, Cards, Loadout, Meta-Währung, Wellen-Rekord)
|
||||
- **Baut auf:** ADR-0002 (nur Quellen werden persistiert; Ablageregel `Resource` vs. `Component`),
|
||||
ADR-0001 (Balancing-Werte sind in die Binary gebacken)
|
||||
|
||||
## Kontext
|
||||
|
||||
ADR-0002 hat die schwierige Hälfte bereits entschieden: Ein Save enthält **niemals einen
|
||||
berechneten Wert**, nur Quellen. Damit ist die Datenmenge klein, flach und formelunabhängig —
|
||||
jede Formeländerung wirkt rückwirkend, ohne Migration. Offen bleiben Format, Ort, Migrationsregel
|
||||
und das Verhalten bei einem kaputten Save.
|
||||
|
||||
Der Spannungspunkt ist der Ort: Er muss plattformüblich sein (Windows, Linux, macOS heute; Mobile
|
||||
und Web später), ohne dass der Spielkern eine Plattform-API sieht.
|
||||
|
||||
**Was Bevy 0.19 mitbringt** (an der Quelle geprüft, nicht aus dem Gedächtnis — Belege unten):
|
||||
Bevy 0.19 hat mit `bevy_settings` ein eingebautes Framework für persistente Nutzerdaten. Es liefert
|
||||
genau die vier Dinge, die man sonst selbst bauen müsste:
|
||||
|
||||
1. **Pfadauflösung für vier Plattformen** über `bevy_platform::dirs::preferences_dir()`.
|
||||
2. **Atomares Schreiben** — erst `<datei>.toml.new`, dann `fs::rename`.
|
||||
3. **Feld-Toleranz beim Laden** — Reflect gleicht Feldnamen ab; fehlende Felder behalten ihren
|
||||
Wert, unbekannte Schlüssel werden ignoriert. Das ist `#[serde(default)]`-Verhalten ohne Attribut.
|
||||
4. **Web-Pfad** über `localStorage` statt Dateisystem, hinter derselben API.
|
||||
|
||||
Das Framework hat drei Lücken, die diese ADR schließen muss: es gibt **keinen Rückkanal** an die
|
||||
App, ob Laden oder Schreiben geklappt hat; auf **Android und iOS** persistiert es nichts; und in
|
||||
`store_fs.rs` fehlt nach einem fehlgeschlagenen `fs::write` ein `return`.
|
||||
|
||||
**Randbedingung aus dem Spielablauf:** Cards sind im laufenden Run über den Pause-Button nur
|
||||
einsehbar, nicht wechselbar; das Loadout steht vor dem Run fest, und Pause friert die gesamte
|
||||
Simulation ein. Damit ändert sich **während eines Runs keine einzige persistierte Quelle**. Das
|
||||
Schreibfenster liegt vollständig zwischen den Runs — es ist keine Politik, sondern eine Folge.
|
||||
|
||||
## Entscheidung
|
||||
|
||||
### 1. Format und Ort: `bevy_settings` mit eigener Fortschrittsdatei
|
||||
|
||||
Der Meta-Fortschritt lebt als `Resource` mit `#[derive(SettingsGroup)]` und
|
||||
`#[settings_group(file = "progress")]` — also in **`progress.toml`**, getrennt von der späteren
|
||||
`settings.toml` für Grafik und Audio. Fortschritt und Optionen haben verschiedene Lebensdauern und
|
||||
verschiedene Löschwege; wer seine Grafikoptionen zurücksetzt, darf nicht seinen Turm verlieren.
|
||||
|
||||
Der App-Name ist die Reverse-Domain der Repo-URL: **`de.hive-brain.mdietermann.infinitower`**.
|
||||
Daraus ergeben sich die Orte:
|
||||
|
||||
| Plattform | Verzeichnis | Quelle |
|
||||
|---|---|---|
|
||||
| Windows | `%LOCALAPPDATA%\<app>\progress.toml` (`FOLDERID_LocalAppData`) | `dirs/windows.rs` |
|
||||
| Linux | `$XDG_CONFIG_HOME/<app>/progress.toml`, sonst `~/.config/...` | `dirs/linux.rs` |
|
||||
| macOS | `~/Library/Preferences/<app>/progress.toml` | `dirs/macos.rs` |
|
||||
| Web | `localStorage` (kein Dateisystem) | `store_wasm.rs` |
|
||||
| Android, iOS | **nichts** — `preferences_dir()` liefert `None` | `dirs/mod.rs` |
|
||||
|
||||
**Das Format ist damit TOML, und zwar nicht frei gewählt, sondern der Preis des Frameworks.** Die
|
||||
Abwägung fällt trotzdem für Klartext aus: Ein Spieler kann `progress.toml` mit jedem Editor
|
||||
aufmachen und sich Workshop-Level schenken. Es gibt kein Leaderboard, keinen Server, keine
|
||||
Ökonomie und genau einen Spieler — der Schaden ist null, und der Nutzen beim Debuggen eines
|
||||
Lernprojekts ist real. Verschleierung (Base64, Prüfsumme, Binärformat) hält keinen auf, der es
|
||||
ernst meint, und kostet genau die Lesbarkeit, die hier den Wert ausmacht. Schummeln wird
|
||||
ausdrücklich akzeptiert, nicht übersehen.
|
||||
|
||||
### 2. Der Sentinel: `version` als erstes Feld
|
||||
|
||||
Das erste Feld ist `version: u32` mit `Default = 0`. Beim Schreiben steht dort **immer** der
|
||||
aktuelle Wert (heute `1`), nie `0`. Weil das Framework der App nicht mitteilt, ob geladen wurde,
|
||||
ist dieses Feld der Rückkanal — er liegt in den Daten statt in der API.
|
||||
|
||||
Nach dem Laden verzweigt genau ein System dreifach:
|
||||
|
||||
| Gelesen | Bedeutung | Verhalten |
|
||||
|---|---|---|
|
||||
| `0` | Es wurde **nichts** geladen: erster Start **oder** unlesbare Datei | Meldung an den Spieler, **kein Schreiben** bis zur ersten bewussten Aktion |
|
||||
| `== CURRENT` | Normalfall | Normal weiter |
|
||||
| `0 < v < CURRENT` | Älterer Stand mit Bedeutungsänderung | Migrationsschritt läuft **vor** dem ersten Schreiben |
|
||||
| `> CURRENT` | Save aus einer neueren Programmversion | Alle Schreibvorgänge für die Sitzung gesperrt, Meldung an den Spieler |
|
||||
|
||||
`0` ist bewusst zweideutig: Der Unterschied zwischen „erster Start" und „konnte nicht lesen" ist
|
||||
plattformübergreifend nicht feststellbar, weil eine Existenzprüfung auf Dateiebene im Web keine
|
||||
Entsprechung hat. Statt die Zweideutigkeit zu verstecken, wird sie ausgesprochen — siehe Punkt 4.
|
||||
|
||||
### 3. Regel: niemals schreiben, bevor bestätigt gelesen wurde
|
||||
|
||||
**Solange nicht feststeht, dass ein vorhandener Stand gelesen wurde, schreibt das Programm nicht.**
|
||||
Diese Regel ist wichtiger als die Wahl des Frameworks und gilt auch, wenn Punkt 1 später fällt.
|
||||
|
||||
Der Grund ist nicht Robustheit, sondern Respekt vor investierter Zeit: Ein Save, der still durch
|
||||
Defaults überschrieben wird, entwertet rückwirkend jede Stunde, die darin steckt — und der Spieler
|
||||
erfährt es nie. Ein Programm, das stattdessen einen Satz sagt und wartet, kann beendet werden,
|
||||
bevor Schaden entsteht, und die Datei liegt noch da.
|
||||
|
||||
### 4. Migration: Feldnamen sind der Vertrag, `version` ist die Notausfahrt
|
||||
|
||||
| Änderung | Was passiert | Nötig |
|
||||
|---|---|---|
|
||||
| Feld kommt hinzu | Schlüssel fehlt in der Datei → `Default` bleibt stehen | nichts |
|
||||
| Feld fällt weg | Schlüssel ist unbekannt → wird ignoriert, verschwindet beim nächsten Schreiben | nichts |
|
||||
| Feld ändert seine **Bedeutung** | Reflect wendet den alten Wert widerspruchslos **falsch** an | **Verbot, siehe unten** |
|
||||
|
||||
Für die ersten beiden Fälle reicht die Feld-Toleranz des Frameworks; ein Versionsfeld wäre dafür
|
||||
überflüssig. Der dritte Fall ist der, den `#[serde(default)]`-artige Toleranz prinzipbedingt nicht
|
||||
abfangen kann — der Schlüssel passt ja.
|
||||
|
||||
**Verbindlich: Eine Bedeutungsänderung erzwingt einen neuen Schlüsselnamen, nie eine
|
||||
Neuinterpretation des alten.** Umbenennen heißt: alter Schlüssel wird ignoriert, neuer Schlüssel
|
||||
startet auf `Default`. Der Spieler verliert **einen Wert**, nicht seinen Spielstand. Nur wenn genau
|
||||
dieser eine Wert nicht verloren gehen darf, wird `version` erhöht und ein expliziter
|
||||
Migrationsschritt geschrieben, der vor dem ersten Schreiben läuft.
|
||||
|
||||
Daraus folgt eine zweite verbindliche Regel: **Die Ablage ist namensbasiert, nie positionsbasiert.**
|
||||
Workshop-Upgrades und Cards stehen als je ein benanntes Feld in der Datei, nicht als Liste mit
|
||||
Indizes. Eine positionsbasierte Liste macht das Einfügen eines Upgrades in der Mitte zu einer
|
||||
stillen Bedeutungsänderung aller nachfolgenden Werte — genau der Fall, den das Verbot verhindern
|
||||
soll, nur unsichtbar.
|
||||
|
||||
`version` wird also von Tag eins mitgeschrieben, obwohl es im Normalbetrieb nichts tut. Der Grund
|
||||
ist, dass man es nicht nachrüsten kann: Eine Datei ohne Versionsfeld ist von `version = 0` nicht zu
|
||||
unterscheiden.
|
||||
|
||||
### 5. Was persistiert wird
|
||||
|
||||
| Feld | Typ | Herkunft |
|
||||
|---|---|---|
|
||||
| `version` | `u32` | Sentinel, siehe Punkt 2 |
|
||||
| `workshop_levels` | Struct mit **einem benannten Feld je Upgrade** | Workshop-Käufe zwischen Runs |
|
||||
| `card_levels` | Struct mit **einem benannten Feld je Card**, `0` = nicht besessen | fasst „besessen" und „Stufe" in einem Feld zusammen |
|
||||
| `equipped_cards` | Liste von Card-Bezeichnern (Namen, keine Indizes) | Loadout, steht vor dem Run fest |
|
||||
| `card_slots` | `u8` | freigeschaltete Slot-Zahl |
|
||||
| *Meta-Währung* | ganzzahlig | **Name offen** — `CONTEXT.md` hat für diese Ressource noch keinen Begriff; der Feldname folgt dem Glossar, sobald er feststeht |
|
||||
| `highest_wave` | `u32` | höchste je erreichte Welle |
|
||||
|
||||
**Kein Zeitstempel.** Ohne Offline-Progress liest ihn niemand, und ein Feld ohne Leser ist die
|
||||
Miniaturausgabe genau des Fehlers, den ADR-0002 verbietet — gespeicherter Zustand, den niemand
|
||||
braucht.
|
||||
|
||||
**Kein laufender Run.** Keine In-Run-Upgrades, kein Wellenstand, keine Coins, kein `EffectiveStats`.
|
||||
|
||||
**Kein `Balance`.** `Balance` ist eine `Resource`, wird aber nie persistiert: Ihre Werte sind laut
|
||||
ADR-0001 in die Binary gebacken, und ein Save darf nicht auf Werte zeigen, die es im aktuellen
|
||||
Build nicht mehr gibt.
|
||||
|
||||
### 6. Verhältnis zur Ablageregel aus ADR-0002
|
||||
|
||||
Die Regel hält, aber sie ist **einseitig** und wird hier präzisiert:
|
||||
|
||||
> Persistiert ⊆ `Resource`. Nicht jede `Resource` ist persistiert.
|
||||
|
||||
`Balance` ist der Gegenbeleg (Punkt 5). Die Markierung ist deshalb nicht „ist eine `Resource`",
|
||||
sondern das `SettingsGroup`-Derive — am Typ ablesbar, nicht aus der Ablage zu erraten.
|
||||
|
||||
**Es gibt genau eine Ausnahme, und sie ist benannt:** `highest_wave` und die im Run verdiente
|
||||
Meta-Währung entstehen *innerhalb* eines Runs, müssen ihn aber überdauern. Sie sind trotzdem keine
|
||||
Run-Daten, sondern Meta-Werte, die aus Run-Daten abgeleitet werden. Verbindlich: **Dieser Übergang
|
||||
findet genau einmal statt, am Run-Ende, in einem einzigen System.** Kein anderes System schreibt
|
||||
aus Run-Zustand in eine persistierte `Resource`. Alles Übrige folgt ADR-0002 unverändert.
|
||||
|
||||
Die Randbedingung „Cards im Run nur einsehbar" entzieht ADR-0002 Punkt 10 nebenbei ihren Fall 1
|
||||
(Card-Wechsel im laufenden Run). Der Abgleich in `sync_tower_abilities` bleibt richtig; er hat nur
|
||||
einen Auslöser weniger. Das ist eine Beobachtung, keine Änderung an ADR-0002.
|
||||
|
||||
### 7. Wann geschrieben wird
|
||||
|
||||
- Nach **jeder** Änderung einer persistierten `Resource`: `SaveSettingsDeferred` (1 s Debounce).
|
||||
- **Zusätzlich** beim Beenden: `SaveSettingsSync::IfChanged`. Beides zusammen, nicht eines von
|
||||
beiden — die Bevy-Doku weist ausdrücklich darauf hin, dass ein Exit-Event auf manchen Plattformen
|
||||
(etwa Cmd-Q auf macOS) nicht abgefangen werden kann.
|
||||
- **Nicht periodisch, nicht bei Pause.** Während eines Runs ändert sich keine persistierte Quelle
|
||||
(Punkt 0 im Kontext), und Pause friert ohnehin alles ein. Es gibt schlicht nichts zu speichern.
|
||||
|
||||
Damit fällt in der Praxis **ein** Schreibvorgang pro Run an (Run-Ende) plus je einer pro Kauf oder
|
||||
Loadout-Änderung zwischen den Runs.
|
||||
|
||||
**Atomares Schreiben ist nötig und kostet hier nichts**, weil das Framework es bereits tut: Bei
|
||||
einer Datei unter 2 KB ist der Preis eine zusätzliche `write`- und eine `rename`-Operation, und der
|
||||
Schutz gegen einen Absturz mitten im Schreiben ist strukturell statt hoffnungsbasiert.
|
||||
|
||||
### 8. Die vier Fehlerfälle
|
||||
|
||||
| Fall | Was technisch passiert | Was der Spieler sieht |
|
||||
|---|---|---|
|
||||
| **Beschädigte Datei** | `decode_toml_file` loggt `Error parsing settings file` und gibt `None`; alle Resources bleiben auf `Default`, also `version == 0` | „Kein Fortschritt geladen. Es wird ein neuer Spielstand angelegt, sobald du fortfährst." — und bis dahin wird **nicht** geschrieben (Punkt 3) |
|
||||
| **Halb geschriebene Datei nach Absturz** | Kann `progress.toml` nicht erreichen: geschrieben wird `progress.toml.new`, erst `fs::rename` macht es gültig. Ein Absturz hinterlässt den alten Stand plus eine verwaiste `.new` | nichts — der alte Stand lädt normal |
|
||||
| **Save aus neuerer Programmversion** | Unbekannte Schlüssel werden ignoriert, bekannte angewandt; `version > CURRENT` fängt das ab | „Dieser Spielstand stammt aus einer neueren Version." Alle Schreibvorgänge sind für die Sitzung gesperrt, die Datei bleibt unangetastet |
|
||||
| **Fehlende Schreibrechte** | `fs::write` bzw. `fs::rename` schlagen fehl, werden geloggt, und die App erfährt es nicht | **Nichts** — und das ist die ehrliche Lücke dieser Entscheidung. Es gibt in 0.19 keinen Rückkanal. Deshalb zeigt der Slice ausdrücklich **keine** „Gespeichert"-Bestätigung: eine Erfolgsmeldung, die das Programm nicht belegen kann, ist schlimmer als gar keine |
|
||||
|
||||
Der Fall, der bestehenden Fortschritt aktiv zerstören kann, ist keiner dieser vier, sondern eine
|
||||
Verkettung: In `store_fs.rs` fehlt nach einem fehlgeschlagenen `fs::write` ein `return`, sodass
|
||||
`fs::rename` die möglicherweise abgeschnittene Temp-Datei trotzdem über den intakten Save schiebt.
|
||||
Auslöser wäre eine volle Platte oder ein Quota. Der Schutz ist einseitig: Parst die abgeschnittene
|
||||
Datei nicht mehr, fängt sie Fall 1 ab und es wird nichts überschrieben. **Ob eine abgeschnittene
|
||||
TOML-Datei immer ungültig ist, wurde nicht geprüft und ist unwahrscheinlich** — eine Datei, die
|
||||
nach einer vollständigen Zeile endet, parst tadellos und hat den Rest still auf `Default`. Genau
|
||||
deshalb steht dieser Punkt unten als Revisions-Auslöser und nicht als gelöstes Problem.
|
||||
|
||||
## Belegtiefe
|
||||
|
||||
Jede tragende Behauptung mit ihrer Quelle. Alle Dateipfade beziehen sich auf das Bevy-Repo am Tag
|
||||
**`v0.19.0`**.
|
||||
|
||||
| Behauptung | Beleg | Status |
|
||||
|---|---|---|
|
||||
| `bevy_settings` existiert im Engine-Repo, erreichbar als `bevy::settings` hinter dem **nicht-default** Feature `bevy_settings` | `crates/bevy_settings/src/lib.rs`; docs.rs `bevy/0.19.0/bevy/settings/`; Feature-Liste docs.rs `crate/bevy/0.19.0/features` | belegt |
|
||||
| Attribut `#[settings_group(file = "<name>")]`, Default `settings`, ergibt `<name>.toml` | `lib.rs`, Doc-Kommentar am Trait `SettingsGroup`; verwendet im Test `#[settings_group(file = "audio")]` | belegt |
|
||||
| `preferences_dir()` liefert auf Android und iOS `None` | `crates/bevy_platform/src/dirs/mod.rs`: `#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] pub fn preferences_dir() -> Option<PathBuf> { None }`. `store_fs.rs` setzt daraufhin `base_path: None`; `save` und `load` sind dann No-Ops mit `warn!("Could not find user configuration directories")` | belegt |
|
||||
| Pfade: Windows `FOLDERID_LocalAppData`, macOS `~/Library/Preferences`, Linux `$XDG_CONFIG_HOME` bzw. `~/.config` | `dirs/windows.rs` (`SHGetKnownFolderPath`), `dirs/macos.rs`, `dirs/linux.rs` | belegt |
|
||||
| Atomares Schreiben über Temp-Datei plus `rename` | `store_fs.rs`: `let temp_path = base_path.join(format!("{filename}.toml.new"));` … `fs::rename(&temp_path, file_path)` | belegt |
|
||||
| In `store_fs.rs` fehlt nach fehlgeschlagenem `fs::write` ein `return`, `fs::rename` läuft trotzdem | `store_fs.rs`, in `save` und `save_async`: `if let Err(e) = fs::write(&temp_path, …) { error!(…); }` — ohne `return`, direkt gefolgt vom `fs::rename` | belegt **durch Code-Lesen**; ein tatsächlicher Datenverlust wurde nicht beobachtet und der Upstream-Bugtracker nicht geprüft |
|
||||
| Kein Rückkanal an die App über Lade- oder Schreiberfolg | `SettingsStore::{save, save_async, load}` sind `pub(crate)` und geben nichts zurück; `load_settings_file` gibt `()` zurück; das Modul exportiert kein Fehler-Event und keine Status-Resource | belegt |
|
||||
| Fehlerbehandlung ist **`error!`** für Lese- und Parse-Fehler, **`warn!`** für fehlende Datei und fehlendes Verzeichnis | `store_fs.rs`: `error!("Error reading settings file: …")`, `error!("Error parsing settings file: …")`; `lib.rs`: `warn!("Filename {filename}.toml not found")` | belegt — **korrigiert** gegenüber meiner früheren, ungenauen Aussage „nur `warn!`" |
|
||||
| Fehlende Felder behalten ihren Wert, unbekannte Schlüssel werden ignoriert | `lib.rs`, `load_properties`, Kommentar wörtlich: *„Deserialize matching field names, ignore ones that don't match."* — die Schleife läuft über die Reflect-Feldnamen und schlägt jeden in der TOML-Tabelle nach | belegt |
|
||||
| Deferred-Save mit 1 s Debounce; `SaveSettingsSync::IfChanged`; Empfehlung, beides zu nutzen | `lib.rs`: `SaveSettingsDeferred(pub Duration)` mit `Duration::from_secs(1)` als `Default`; `SaveSettingsSync::IfChanged`; Doc-Kommentar am `SettingsPlugin` zum Cmd-Q-Fall auf macOS | belegt |
|
||||
| Persistierte Typen brauchen `#[reflect(Resource, SettingsGroup, Default)]` | `lib.rs` ruft `ty.data::<ReflectDefault>().unwrap()`, wenn die Resource noch nicht existiert — ein fehlendes `ReflectDefault` ist damit ein Panic, kein stiller Fehler | belegt |
|
||||
| Ob eine abgeschnittene TOML-Datei zuverlässig ungültig ist | — | **nicht geprüft, wird nicht angenommen** |
|
||||
| Ob `bevy_settings` in einer späteren Bevy-Version einen Rückkanal bekommt | — | **nicht geprüft** |
|
||||
|
||||
## Verworfene Alternativen
|
||||
|
||||
- **Eigenes Save-Modul mit `ron` und `serde`** (beide laut ADR-0001 bereits im `Cargo.lock`). Der
|
||||
Rückkanal wäre ein echter Gewinn — man hielte `io::Result` in der Hand. Der Preis ist alles
|
||||
andere: eine eigene `preferences_dir()`-Entsprechung für drei Betriebssysteme (Bevys Version ist
|
||||
nach `dirs-sys` modelliert und braucht auf Windows `unsafe` um `SHGetKnownFolderPath`), ein
|
||||
eigener `localStorage`-Pfad für Web, eigenes Temp-and-Rename und eine eigene Feld-Toleranz — für
|
||||
sechs Felder Nutzdaten. Dazu bliebe es dauerhaft eigene Wartungslast, während der `store_fs`-Bug
|
||||
stromaufwärts gefixt wird und beim nächsten Bevy-Bump gratis ankommt.
|
||||
- **`dirs` (6.0.0) oder `directories` als Dependency plus eigenes Schreiben.** Löst nur den Ort,
|
||||
nicht Web, nicht Atomarität, nicht die Feld-Toleranz — und `bevy_platform::dirs` ist ohnehin nach
|
||||
demselben Vorbild gebaut und liegt bereits im Baum.
|
||||
- **Alles in `settings.toml`, ohne eigene Datei.** Spart ein Attribut und koppelt Fortschritt an
|
||||
Grafikoptionen. „Einstellungen zurücksetzen" würde den Spielstand mitnehmen.
|
||||
- **Verschlüsselung oder Prüfsumme gegen Schummeln.** Kauft nichts (Einzelspieler, kein
|
||||
Vergleichswert verlässt das Gerät) und kostet die Lesbarkeit, die beim Debuggen den Wert ausmacht.
|
||||
- **Positionsbasierte Listen für Workshop-Level und Cards.** Kompakter in der Datei, aber ein
|
||||
eingefügtes Upgrade verschiebt still die Bedeutung aller folgenden Werte — die Fehlerklasse, die
|
||||
Punkt 4 verbietet, nur ohne Symptom.
|
||||
- **Speichern bei jeder Änderung ohne Debounce.** Unnötig: Zwischen den Runs ändert sich wenig, im
|
||||
Run nichts, und `SaveSettingsDeferred` fasst Klick-Serien im Workshop ohnehin zusammen.
|
||||
|
||||
## Konsequenzen
|
||||
|
||||
**Positiv**
|
||||
|
||||
- Keine deklarierte Dependency; nur ein Cargo-Feature (siehe unten).
|
||||
- Der Spielkern sieht keine Plattform-API — nur ein `#[derive]` und eine `Resource`. Die
|
||||
Plattformfallunterscheidung liegt vollständig in `bevy_settings`.
|
||||
- Web funktioniert ohne eigenen Code, weil `localStorage` hinter derselben API liegt.
|
||||
- Atomares Schreiben, Feld-Toleranz und Pfadauflösung sind fremdgewartet.
|
||||
- Der Save ist im Editor lesbar — ein Fehlerbild lässt sich in Sekunden verifizieren.
|
||||
|
||||
**Negativ**
|
||||
|
||||
- **Android und iOS persistieren nichts** und melden es nur als `warn!`. Der Mobile-Port erbt ein
|
||||
stumm kaputtes Save/Load und braucht eigene Arbeit. `CONTEXT.md` nennt Mobile als nächsten Schritt
|
||||
nach dem Slice.
|
||||
- Ein fehlgeschlagener Schreibvorgang ist für die App unsichtbar. Deshalb gibt es keine
|
||||
„Gespeichert"-Anzeige — der Slice verzichtet lieber auf Rückmeldung als auf Wahrheit.
|
||||
- `version == 0` kann „erster Start" oder „kaputte Datei" heißen. Die Meldung an den Spieler muss
|
||||
beides abdecken und darf keines von beidem behaupten.
|
||||
- Ein *Settings*-Framework trägt Save-Daten. Das passt, solange der Fortschritt aus flachen
|
||||
Skalaren besteht (ADR-0002 Punkt 5). Sobald ein Save Entities oder Weltzustand enthielte, wäre es
|
||||
das falsche Werkzeug — und ADR-0002 müsste zuerst fallen.
|
||||
- Die persistierten Typen sind an `Reflect` gebunden; ein vergessenes `#[reflect(… Default)]` ist
|
||||
ein Panic beim Start statt eines Compile-Fehlers.
|
||||
|
||||
**Neue Dependencies**
|
||||
|
||||
Keine deklarierte Dependency. Nötig ist das **nicht-default Cargo-Feature `bevy_settings`**:
|
||||
|
||||
```toml
|
||||
bevy = { version = "0.19.0", features = ["bevy_settings"] }
|
||||
```
|
||||
|
||||
Transitiv kommt dadurch genau **eine** Kiste neu in den `Cargo.lock`: die Fassade **`toml`
|
||||
(^1.1.0)**. Ihr Unterbau liegt bereits dort (`toml_edit` 0.25.13, `toml_parser` 1.1.3,
|
||||
`toml_datetime` 1.1.1), ebenso `serde`, `thiserror`, `web-sys` und `windows-sys`. Der Zuwachs an
|
||||
Compile-Zeit ist damit auf eine dünne Fassadenkiste begrenzt — das ist der Grund, warum diese
|
||||
Entscheidung nicht gegen die Dependency-Zurückhaltung aus ADR-0001 verstößt.
|
||||
|
||||
**Folgeaufgaben** (nicht Teil dieser ADR)
|
||||
|
||||
- Der Feldname für die Meta-Währung folgt, sobald `CONTEXT.md` den Begriff festlegt.
|
||||
- Die Reihenfolge der Plugins: `SettingsPlugin` lädt beim Hinzufügen, also vor allem, was die
|
||||
geladenen Werte lesen will.
|
||||
|
||||
## Revisions-Auslöser
|
||||
|
||||
Diese Entscheidung wird neu getroffen, sobald **eines** der folgenden Ereignisse eintritt:
|
||||
|
||||
1. **Truncated-Promote beobachtet:** Ein Log-Eintrag `Error saving settings file` erscheint, **oder**
|
||||
eine Datei `progress.toml.new` bleibt im Einstellungsverzeichnis liegen. Beides ist der direkte
|
||||
Beleg, dass der fehlende `return` in `store_fs.rs` gegriffen hat. Dann wird das Schreiben
|
||||
übernommen — Eigenbau nur für den Schreibpfad, Laden bleibt beim Framework.
|
||||
2. **Mobile im Scope:** Sobald ein Android- oder iOS-Build gebaut werden soll. `preferences_dir()`
|
||||
liefert dort `None`, und der Fortschritt verschwindet lautlos. Kein Grenzwert, ein Build reicht.
|
||||
3. **Ein Save muss mehr als flache Skalare enthalten** — Entities, Weltzustand, ein laufender Run.
|
||||
Dann ist nicht dieses Format falsch, sondern zuerst ADR-0002 Punkt 5 neu zu bewerten.
|
||||
4. **Ein Vergleichswert verlässt das Gerät** (Leaderboard, geteilter Wellen-Rekord, Cloud-Save).
|
||||
Ab diesem Moment hat Schummeln ein Opfer, und die Klartext-Entscheidung aus Punkt 1 ist neu zu
|
||||
treffen — nicht vorher.
|
||||
5. **Eine zweite Bedeutungsänderung innerhalb eines Slice** erzwingt eine `version`-Erhöhung. Einmal
|
||||
ist eine Notausfahrt; zweimal heißt, dass die Feldnamen den Vertrag nicht mehr tragen, und dann
|
||||
wird ein echtes Migrationsschema fällig statt einer Umbenennungsregel.
|
||||
Reference in New Issue
Block a user