09 — Save-Schema #10

Closed
opened 2026-08-11 13:13:28 +00:00 by RDietermann · 1 comment
Owner

Teil der Map #1 · Type: wayfinder:grilling · Blocked by: #8

Was steht in einem Speicherstand, und wie ueberlebt er geaenderte Mods?

Volle Fragestellung (kanonisch): .scratch/rustkeep-1.0/issues/09-save-schema.md

Teil der Map #1 · **Type: `wayfinder:grilling`** · **Blocked by: #8** Was steht in einem Speicherstand, und wie ueberlebt er geaenderte Mods? Volle Fragestellung (kanonisch): `.scratch/rustkeep-1.0/issues/09-save-schema.md`
RDietermann added the wayfinder:grilling label 2026-08-11 13:13:28 +00:00
Author
Owner

Aufgeloest in bed9ef5. Kanonische Fassung: .scratch/rustkeep-1.0/issues/09-save-schema.md

Die Etage wird gespeichert, nicht gewuerfelt

#15 hat einen seedbaren Generator geliefert, Regenerieren waere also moeglich. Drei Dinge sprechen dagegen:

  1. Regenerieren verlangt dieselben Prefabs und denselben Generator-Code — ein Mod-Update erzeugt ein anderes Layout, und der gespeicherte Held steht dann in einer Wand.
  2. Der Raumzustand aus #11 (cleared, gefallene Gegner, genommene Beute) laesst sich nicht regenerieren und muss ohnehin mit. Der Seed spart also nur das Kachelgitter.
  3. Die Rechnung: 80x40 Felder sind als ASCII-Zeilen rund 3 KB — im selben Format wie Prefabs, lesbar und im Notfall von Hand reparierbar.

Der Seed bleibt im Save, aber fuer kuenftige Wuerfe (#15), nicht zum Regenerieren.

Ein Slot, Permadeath

Autosave beim Betreten einer Etage und beim Beenden. Kein manuelles Speichern: es braechte nur die Wahl des Zeitpunkts, und die hat #15 mit dem Seed bereits abgeraeumt. Mehrere Slots waeren eine Verwaltungsansicht fuer ein Spiel mit einem Helden.

Speichern im Kampf erlaubt (passiert beim Beenden ohnehin, Kampfzustand faellt bei serde-Ableitung gratis mit ab). Bei Tod wird geloescht — aber erst beim Erscheinen des GameOver-Screens, nicht beim Fallen der Lebenspunkte. Stuerzt das Spiel dazwischen ab, ist der letzte Autosave noch da.

Der Save ist der Game-Baum

#[derive(Serialize, Deserialize)] durchgehend, mit einer handgeschriebenen Ausnahme: dem Kachelgitter als ASCII-Zeilen statt eines Arrays mit 3200 Eintraegen. Eigene Save-Structs hiessen, jede Struktur zweimal zu pflegen — das Duplikat, das genau dann auseinanderlaeuft, wenn es niemand merkt.

[save]
schema = 1              # das einzige Feld, das die Engine auswertet
game   = "0.1.0"        # Diagnose bei Fehlermeldungen

[[save.mods]]
id = "core"
version = "1.0.0"

Bewusst kein floor/turn im Header — der Baum serialisiert beide ein paar Zeilen weiter unten, und einen Speicherstand-Browser gibt es bei einem Slot nicht.

Bei abweichender schema: ablehnen mit Klartextmeldung, Datei bleibt liegen. Migration zu schreiben, bevor ein einziger Speicherstand ausgeliefert wurde, ist Code fuer ein Problem, das niemand hat; Best-Effort auf strukturell anderem Schema erzeugt stille Verfaelschung statt eines Fehlers. Und eine Datei zu loeschen, weil man sie nicht lesen kann, waere die schlechteste aller Reaktionen.

Fehlende Mods: verwerfen, nie ablehnen

Fehlt Folge
Gegenstand im Inventar Eintrag entfaellt
Gegenstand in der Ausruestung Slot bleibt leer
Affix entfaellt, Gegenstand bleibt
Quest Fortschritt entfaellt (#9)
Prefab egal — das Layout ist gespeichert
Charakteroption entfaellt, Held bleibt spielbar

Ein Spieler soll nicht seinen ganzen Lauf verlieren, weil er einen Mod deinstalliert hat.

Die Mod-Version wird verglichen und protokolliert, mehr nicht. Verschwundene IDs fangen die Regeln oben ab, geaenderte Werte gelten einfach. Der Vergleich leistet reine Diagnose — eine Zeile "meinmod 1.0.0 -> 1.2.0 seit dem Speichern", die genau dann Gold wert ist, wenn jemand meldet "mein Spielstand ist seit dem Update kaputt". Das ist die ehrliche Bilanz des version-Felds aus #4.

Warnungen gehen in dieselbe Sitzungsdatei wie der Mod-Ladelauf (zwei Dateinamensmuster hiessen zwei Rotationsregeln), plus eine Rueckfrage vor dem Betreten des Spiels: "12 Eintraege fehlen. Fortfahren?" Das weicht bewusst von #5s Regel "eine Zeile, kein Screen" ab — der Spieler will an dieser Stelle vielleicht den fehlenden Mod nachinstallieren statt weiterzuspielen, und diese Wahl hat er nur vorher.

Atomar schreiben

data_dir()/rustkeep/save.toml, geschrieben nach save.toml.tmp und per fs::rename darueberzogen — das ersetzt die Zieldatei auf Windows 10 1607+ wie auf Unix (gegen die std-Doku geprueft).

Die eine Stelle in diesem Ticket, an der bewusst nicht lazy entschieden wurde. Der Autosave ueberschreibt den einzigen Speicherstand, den es gibt; bricht das Schreiben ab, ist ein Lauf von einer Stunde weg. Zwei Zeilen, und es liegt entweder die alte Datei da oder die neue, nie eine halbe.

Gold braucht kein eigenes Feld — es ist seit #2 ein stapelbarer Gegenstand und liegt im Inventar.

Zwei Nachtraege

  • #3 (02): core:attack und core:flee sind jedem Wesen implizit gewaehrt. Faehigkeiten kamen bisher ausschliesslich von Charakteroptionen und Gegenstaenden — ein Held ohne Klasse und ohne Waffe haette gar keine und koennte nicht einmal angreifen. Keine Sonderregel fuer den Ladefall: es trifft auch jeden Gegner ohne abilities-Eintrag, vor dessen leerer Liste die KI aus #15 stuende.
  • #12 (11): Confirm { text, on_yes: Box<Response> } statt on_yes: Command. Die Rueckfrage beim Laden braucht keinen Command, sondern einen Screen-Wechsel; Response::Command ist bereits eine Variante, der Sonderfall entfaellt ersatzlos und die Struktur wird dabei kleiner.
**Aufgeloest** in bed9ef5. Kanonische Fassung: `.scratch/rustkeep-1.0/issues/09-save-schema.md` ### Die Etage wird gespeichert, nicht gewuerfelt #15 hat einen seedbaren Generator geliefert, Regenerieren *waere* also moeglich. Drei Dinge sprechen dagegen: 1. Regenerieren verlangt **dieselben Prefabs und denselben Generator-Code** — ein Mod-Update erzeugt ein anderes Layout, und der gespeicherte Held steht dann in einer Wand. 2. Der Raumzustand aus #11 (`cleared`, gefallene Gegner, genommene Beute) laesst sich nicht regenerieren und muss ohnehin mit. Der Seed spart also nur das Kachelgitter. 3. Die Rechnung: 80x40 Felder sind als ASCII-Zeilen **rund 3 KB** — im selben Format wie Prefabs, lesbar und im Notfall von Hand reparierbar. Der Seed bleibt im Save, aber fuer kuenftige Wuerfe (#15), nicht zum Regenerieren. ### Ein Slot, Permadeath Autosave beim **Betreten einer Etage** und beim **Beenden**. Kein manuelles Speichern: es braechte nur die Wahl des Zeitpunkts, und die hat #15 mit dem Seed bereits abgeraeumt. Mehrere Slots waeren eine Verwaltungsansicht fuer ein Spiel mit einem Helden. Speichern im Kampf erlaubt (passiert beim Beenden ohnehin, Kampfzustand faellt bei serde-Ableitung gratis mit ab). **Bei Tod wird geloescht** — aber erst beim Erscheinen des `GameOver`-Screens, nicht beim Fallen der Lebenspunkte. Stuerzt das Spiel dazwischen ab, ist der letzte Autosave noch da. ### Der Save *ist* der `Game`-Baum `#[derive(Serialize, Deserialize)]` durchgehend, mit **einer** handgeschriebenen Ausnahme: dem Kachelgitter als ASCII-Zeilen statt eines Arrays mit 3200 Eintraegen. Eigene Save-Structs hiessen, jede Struktur zweimal zu pflegen — das Duplikat, das genau dann auseinanderlaeuft, wenn es niemand merkt. ```toml [save] schema = 1 # das einzige Feld, das die Engine auswertet game = "0.1.0" # Diagnose bei Fehlermeldungen [[save.mods]] id = "core" version = "1.0.0" ``` Bewusst **kein** `floor`/`turn` im Header — der Baum serialisiert beide ein paar Zeilen weiter unten, und einen Speicherstand-Browser gibt es bei einem Slot nicht. Bei abweichender `schema`: **ablehnen** mit Klartextmeldung, **Datei bleibt liegen**. Migration zu schreiben, bevor ein einziger Speicherstand ausgeliefert wurde, ist Code fuer ein Problem, das niemand hat; Best-Effort auf strukturell anderem Schema erzeugt stille Verfaelschung statt eines Fehlers. Und eine Datei zu loeschen, weil man sie nicht lesen kann, waere die schlechteste aller Reaktionen. ### Fehlende Mods: verwerfen, nie ablehnen | Fehlt | Folge | |---|---| | Gegenstand im Inventar | Eintrag entfaellt | | Gegenstand in der Ausruestung | Slot bleibt leer | | Affix | entfaellt, Gegenstand bleibt | | Quest | Fortschritt entfaellt (#9) | | Prefab | egal — das Layout ist gespeichert | | Charakteroption | entfaellt, Held bleibt spielbar | Ein Spieler soll nicht seinen ganzen Lauf verlieren, weil er einen Mod deinstalliert hat. **Die Mod-Version wird verglichen und protokolliert, mehr nicht.** Verschwundene IDs fangen die Regeln oben ab, geaenderte Werte gelten einfach. Der Vergleich leistet reine Diagnose — eine Zeile "`meinmod` 1.0.0 -> 1.2.0 seit dem Speichern", die genau dann Gold wert ist, wenn jemand meldet "mein Spielstand ist seit dem Update kaputt". Das ist die ehrliche Bilanz des `version`-Felds aus #4. Warnungen gehen in **dieselbe Sitzungsdatei** wie der Mod-Ladelauf (zwei Dateinamensmuster hiessen zwei Rotationsregeln), plus eine **Rueckfrage vor dem Betreten des Spiels**: "12 Eintraege fehlen. Fortfahren?" Das weicht bewusst von #5s Regel "eine Zeile, kein Screen" ab — der Spieler will an dieser Stelle vielleicht den fehlenden Mod nachinstallieren statt weiterzuspielen, und diese Wahl hat er nur *vorher*. ### Atomar schreiben `data_dir()/rustkeep/save.toml`, geschrieben nach `save.toml.tmp` und per `fs::rename` darueberzogen — das ersetzt die Zieldatei auf Windows 10 1607+ wie auf Unix (gegen die std-Doku geprueft). **Die eine Stelle in diesem Ticket, an der bewusst nicht lazy entschieden wurde.** Der Autosave ueberschreibt den einzigen Speicherstand, den es gibt; bricht das Schreiben ab, ist ein Lauf von einer Stunde weg. Zwei Zeilen, und es liegt entweder die alte Datei da oder die neue, nie eine halbe. Gold braucht kein eigenes Feld — es ist seit #2 ein stapelbarer Gegenstand und liegt im Inventar. ### Zwei Nachtraege - **#3 (02)**: **`core:attack` und `core:flee` sind jedem Wesen implizit gewaehrt.** Faehigkeiten kamen bisher ausschliesslich von Charakteroptionen und Gegenstaenden — ein Held ohne Klasse und ohne Waffe haette gar keine und koennte nicht einmal angreifen. Keine Sonderregel fuer den Ladefall: es trifft auch jeden Gegner ohne `abilities`-Eintrag, vor dessen leerer Liste die KI aus #15 stuende. - **#12 (11)**: `Confirm { text, on_yes: Box<Response> }` statt `on_yes: Command`. Die Rueckfrage beim Laden braucht keinen Command, sondern einen Screen-Wechsel; `Response::Command` ist bereits eine Variante, der Sonderfall entfaellt ersatzlos und die Struktur wird dabei *kleiner*.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RDietermann/rustkeep#10