docs: ticket 09 (save-schema) aufloesen, nachtraege an 02 und 11, speicherstand ins glossar
This commit is contained in:
@@ -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* |
|
||||
|
||||
Reference in New Issue
Block a user