docs: ticket 09 (save-schema) aufloesen, nachtraege an 02 und 11, speicherstand ins glossar

This commit is contained in:
2026-08-12 12:34:29 +02:00
parent 6442e104ab
commit bed9ef5762
5 changed files with 147 additions and 3 deletions

View File

@@ -1,7 +1,7 @@
# 09 — Save-Schema
Type: grilling
Status: open
Status: resolved
Blocked by: 07
## Question
@@ -33,3 +33,117 @@ Ergebnis: Save-Schema als kommentiertes TOML-Beispiel plus Regeln für fehlende
**eine** Etage, keine besuchten Vorgänger. `Creature.options` (Rasse, Klasse) muss mit, sonst hat der
Held nach dem Laden keine Herkunft mehr — womit die Frage „was, wenn die Klasse fehlt?" oben scharf
wird.
## Answer
Begriffe im [Glossar](../../../CONTEXT.md).
### Die Etage wird gespeichert, nicht gewürfelt
Ticket 14 hat einen seedbaren Generator geliefert, Regenerieren *wäre* also möglich. Drei Dinge
sprechen dagegen:
1. Regenerieren verlangt **dieselben Prefabs und denselben Generator-Code**. Ein Mod-Update oder eine
Engine-Änderung erzeugt ein anderes Layout — und der gespeicherte Held steht dann in einer Wand.
2. Der Raumzustand aus 10 (`cleared`, gefallene Gegner, genommene Beute, Bodenloot) lässt sich nicht
regenerieren und muss ohnehin mit. Der Seed spart also nur das Kachelgitter.
3. Die Rechnung: 80×40 Felder sind als ASCII-Zeilen **rund 3 KB** — im selben Format wie Prefabs, also
lesbar und im Notfall von Hand reparierbar.
Der Seed bleibt trotzdem im Save, aber für künftige Würfe (14), nicht zum Regenerieren.
### Ein Speicherstand, Permadeath
Autosave beim **Betreten einer Etage** und beim **Beenden**. Kein manuelles Speichern: Es brächte nur
die Wahl des Zeitpunkts, und genau die hat Ticket 14 mit dem Seed im Save bereits abgeräumt. Mehrere
benannte Slots wären eine Verwaltungsansicht für ein Spiel mit einem einzigen Helden.
Speichern **im Kampf ist erlaubt**, weil es beim Beenden ohnehin passieren muss; der Kampfzustand fällt
bei serde-Ableitung gratis mit ab.
**Bei Tod wird der Speicherstand gelöscht** — aber erst beim Erscheinen des `GameOver`-Screens, nicht
beim Fallen der Lebenspunkte. Stürzt das Spiel in der Sekunde dazwischen ab, ist der letzte Autosave
noch da, und da er vom Betreten der Etage stammt, verschenkt das nichts.
### Der Save *ist* der `Game`-Baum
`#[derive(Serialize, Deserialize)]` durchgehend, mit **einer** handgeschriebenen Ausnahme: dem
Kachelgitter als ASCII-Zeilen statt eines Arrays mit 3200 Einträgen.
Eigene Save-Structs hießen, jede Struktur zweimal zu pflegen und bei jeder Änderung daran zu denken —
die Sorte Duplikat, die genau dann auseinanderläuft, wenn es niemand merkt. Der Preis ist ehrlich: Der
Save hängt an internen Feldnamen, ein Umbenennen bricht alte Stände. Dafür gibt es die Schema-Version.
```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` und `turn` im Header: Der `Game`-Baum serialisiert beide ohnehin ein paar
Zeilen weiter unten. Sie oben zu wiederholen hätte nur einem Speicherstand-Browser genützt, der sie
ohne Parsen des Rumpfs anzeigen will — und den gibt es bei einem Slot nicht.
Bei abweichender `schema`: **ablehnen** mit Klartextmeldung („Speicherstand stammt aus Schema 1, dieses
Spiel liest 2"), und die **Datei bleibt liegen**. Migration zu schreiben, bevor ein einziger
Speicherstand ausgeliefert wurde, ist Code für ein Problem, das noch niemand hat; Best-Effort auf einem
strukturell anderen Schema erzeugt stille Verfälschung statt eines Fehlers. Und eine Datei zu löschen,
weil man sie nicht lesen kann, wäre die schlechteste aller Reaktionen.
### Fehlende Mods: verwerfen, nie ablehnen
| Fehlt | Folge |
|---|---|
| Gegenstand im Inventar | Eintrag entfällt |
| Gegenstand in der Ausrüstung | Slot bleibt leer |
| Affix | entfällt, Gegenstand bleibt |
| Quest | Fortschritt entfällt (bereits in 08) |
| Prefab | egal — das Layout ist gespeichert |
| Charakteroption | entfällt, 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, geänderte 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 Ticket 03:
Fiele es weg, verlöre man diese Zeile und sonst nichts.
Warnungen gehen in **dieselbe Sitzungsdatei** wie die des Mod-Ladelaufs, nicht in eine eigene
`save-*.log`-Familie: Zwei Dateinamensmuster hießen zwei Rotationsregeln und zwei Orte zum Nachsehen.
Die Warnliste aus 04 lebt stattdessen für die ganze Sitzung und wird neu geschrieben, wenn sie wächst
— das ist sogar weniger Logik als zwei getrennte Läufe.
Dazu eine **Rückfrage vor dem Betreten des Spiels**: „12 Einträge fehlen (siehe `logs/load-….log`).
Fortfahren?"
Das weicht bewusst von 04s Regel „eine Zeile, kein Screen" ab, und zwar aus einem Grund: Der Spieler
will an dieser Stelle vielleicht gar nicht weiterspielen, sondern den fehlenden Mod nachinstallieren.
Diese Wahl hat er nur *vor* dem Laden — danach ist der verkrüppelte Held gespeichert.
### Atomar schreiben
`data_dir()/rustkeep/save.toml`, dasselbe Verzeichnis wie Mods und `load_order.toml` (03 hat die
Aufteilung in Daten und Konfiguration verworfen). Geschrieben wird nach `save.toml.tmp` und dann per
`fs::rename` darübergezogen — das ersetzt die Zieldatei auf Windows 10 1607+ ebenso wie auf Unix
(gegen die std-Doku geprüft).
**Das ist die eine Stelle in diesem Ticket, an der bewusst nicht lazy entschieden wurde.** Der Autosave
überschreibt den einzigen Speicherstand, den es gibt; bricht das Schreiben ab — Absturz, voller
Datenträger, geschlossener Deckel —, ist ein Lauf von einer Stunde weg. Temporärdatei plus `rename`
sind zwei Zeilen und machen den Verlust unmöglich: Es liegt entweder die alte Datei da oder die neue,
nie eine halbe.
**Gold braucht kein eigenes Feld** — es ist seit Ticket 01 ein gewöhnlicher stapelbarer Gegenstand
(`core:gold`) und liegt im Inventar wie alles andere.
### Zwei Nachträge an geschlossene Tickets
| Ticket | Nachtrag |
|---|---|
| **02** | **`core:attack` und `core:flee` sind jedem Wesen implizit gewährt.** Fähigkeiten kommen bisher ausschließlich von Charakteroptionen und Gegenständen — ein Held, dessen Klasse fehlt und der nichts trägt, hätte gar keine und könnte nicht einmal angreifen. Das ist keine Sonderregel für den Ladefall: Auch jeder Gegner ohne `abilities`-Eintrag trifft es, und die KI aus 14 stünde vor einer leeren Liste |
| **11** | `Confirm { text, on_yes: Box<Response> }` statt `on_yes: Command`. Die Rückfrage beim Laden braucht keinen Command, sondern einen Screen-Wechsel. `Response::Command` ist bereits eine Variante, der bisherige Fall bleibt also erhalten — der Sonderfall entfällt ersatzlos, die Struktur wird dabei *kleiner* |