docs: ticket 04 (fehlerstrategie) aufloesen, vorgaben an ticket 11
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# 04 — Fehlerstrategie für kaputte Mods
|
# 04 — Fehlerstrategie für kaputte Mods
|
||||||
|
|
||||||
Type: grilling
|
Type: grilling
|
||||||
Status: open
|
Status: resolved
|
||||||
Blocked by: 03
|
Blocked by: 03
|
||||||
|
|
||||||
## Question
|
## Question
|
||||||
@@ -32,3 +32,110 @@ Offen bleibt damit: kaputte TOML-Syntax (die Datei ist unlesbar, die Einheit „
|
|||||||
fehlende Pflichtfelder, unaufgelöste Referenzen aus Phase 3 — und wohin die Warnungen gehen, wenn der
|
fehlende Pflichtfelder, unaufgelöste Referenzen aus Phase 3 — und wohin die Warnungen gehen, wenn der
|
||||||
Bildschirm ratatui gehört. Ein Ladelauf produziert leicht ein Dutzend, und `println!` ist nach dem
|
Bildschirm ratatui gehört. Ein Ladelauf produziert leicht ein Dutzend, und `println!` ist nach dem
|
||||||
TUI-Start keine Option mehr.
|
TUI-Start keine Option mehr.
|
||||||
|
|
||||||
|
## Answer
|
||||||
|
|
||||||
|
### Zwei Klassen, zwei Einheiten
|
||||||
|
|
||||||
|
**Syntax ist ein Datei-Fehler, alles Semantische ein Eintrags-Fehler.**
|
||||||
|
|
||||||
|
Das ist nicht gratis: `toml::from_str::<ModFile>()` mit `items: Vec<Item>` scheitert *komplett*, wenn
|
||||||
|
ein einziger Eintrag ein kaputtes Feld hat — das Versprechen aus Ticket 03 wäre damit genau dann
|
||||||
|
gebrochen, wenn 200 Gegenstände in einer Datei stehen, also im Normalfall. Deshalb wird zweistufig
|
||||||
|
deserialisiert: erst `Vec<Spanned<toml::Value>>` je Registry-Tabelle, dann jeder Eintrag einzeln in
|
||||||
|
seinen Typ. Etwa zehn Zeilen, und ein falsches Feld trifft nur noch seinen Eintrag.
|
||||||
|
|
||||||
|
Bei kaputter TOML-Syntax fällt die ganze Datei — da kommt der Parser nicht weiter, das ist keine
|
||||||
|
Entscheidung, sondern eine Tatsache.
|
||||||
|
|
||||||
|
### Harte Fehler — genau drei
|
||||||
|
|
||||||
|
`depends` nicht installiert · `depends`-Reihenfolge verletzt · `core` nicht gefunden. Alle drei:
|
||||||
|
Meldung auf stderr, `exit(1)`. Das TUI startet gar nicht erst, damit ist stderr genau der richtige Ort.
|
||||||
|
|
||||||
|
`core` ist darüber hinaus **kein Sonderfall**: Ein kaputter Eintrag darin ist eine Warnung wie überall.
|
||||||
|
Ein Sonderfall würde die Privilegierung wieder einbauen, die Ticket 03 gerade abgeschafft hat. Die
|
||||||
|
Absicherung gehört nicht in die Laufzeit, sondern in einen Test — siehe unten. Dass `core` fehlend
|
||||||
|
hart ist, widerspricht dem nicht: Ohne `core` gibt es kein Attribut, keinen Slot und keinen
|
||||||
|
Gegenstand, das ist nicht Privileg, sondern die Abwesenheit sämtlichen Inhalts.
|
||||||
|
|
||||||
|
Ein nicht schreibbares Log ist ausdrücklich **kein** harter Fehler. Der Schreibfehler wird verschluckt;
|
||||||
|
ein Spiel, das wegen seiner Logdatei nicht startet, wäre die Karikatur guter Fehlerbehandlung.
|
||||||
|
|
||||||
|
### Warnungen
|
||||||
|
|
||||||
|
Während des Ladens in einem `Vec<String>` gesammelt, am Ende nach
|
||||||
|
`data_dir()/rustkeep/logs/load-20260812-112305.log` geschrieben. Die letzten zehn bleiben liegen,
|
||||||
|
ältere werden beim Schreiben gelöscht — streng auf das Muster `load-*.log` im eigenen `logs/`-Ordner
|
||||||
|
beschränkt. Ein sauberer Lauf legt keine Datei an.
|
||||||
|
|
||||||
|
Auf dem Startbildschirm steht **eine Zeile** mit Anzahl und Dateiname. Kein Report-Screen: Das wäre
|
||||||
|
ein Screen mit Scrolling und Navigation, den Ticket 11 mitschleppen müsste, für Inhalt, den ein
|
||||||
|
Texteditor besser anzeigt.
|
||||||
|
|
||||||
|
Keine Schweregrade. Jeder Fall aus 03 und aus der Deserialisierung ist „Warnung, verworfen" — eine
|
||||||
|
zweite Stufe wäre ein Feld, das überall denselben Wert trägt, plus die Frage, was `INFO` auslöst.
|
||||||
|
|
||||||
|
### Meldungsformat
|
||||||
|
|
||||||
|
```
|
||||||
|
meinmod/items.toml:42: core:iron_sword — unknown field `base_pric`
|
||||||
|
```
|
||||||
|
|
||||||
|
Die **Mod-ID steht vorne**: Bei zwölf installierten Mods ist sie die einzige Angabe, mit der ein
|
||||||
|
Spieler weiß, wen er anschreiben muss.
|
||||||
|
|
||||||
|
`toml::de::Error` liefert `span() -> Option<Range<usize>>` und `message()`, aber keine Zeile — die ist
|
||||||
|
`input[..span.start].lines().count()`, eine Zeile Code. Keine Spalte: `message()` nennt das Feld
|
||||||
|
bereits im Klartext, die Spalte wäre doppelt gemoppelt und bräuchte eine zweite Rechnung. Ein Eintrag
|
||||||
|
ohne lesbare `id` kann sich nicht benennen und heißt nach seiner Position: `items[3]`.
|
||||||
|
|
||||||
|
### Rust-Seite
|
||||||
|
|
||||||
|
Handgeschriebenes `enum LoadError` mit drei Varianten und eigenem `impl Display`.
|
||||||
|
|
||||||
|
`thiserror` wäre ein Proc-Macro-Dependency (zieht `syn` und `quote` in den Build) für ungefähr zehn
|
||||||
|
gesparte Zeilen `match` — und in einem Ticket, das ausdrücklich Rust lernen soll, ist das Schreiben
|
||||||
|
dieses `match` der Zweck. `anyhow` ist für Anwendungen gedacht, denen der konkrete Fehlertyp egal ist;
|
||||||
|
hier ist er es nicht, es sind genau drei benannte Fälle. `impl std::error::Error` bleibt weg, solange
|
||||||
|
niemand `Box<dyn Error>` braucht.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn load_mods() -> Result<(Registries, Vec<String>), LoadError>
|
||||||
|
```
|
||||||
|
|
||||||
|
Warnungen reisen als zweiter Rückgabewert. Als Feld in `Registries` würden sie Diagnose an die
|
||||||
|
Spieldaten hängen, die danach das ganze Spiel lang mitfahren und in jedem Save-Gedanken auftauchen;
|
||||||
|
als `&mut Vec<String>`-Parameter wäre es das C-Muster. Ein Tupel aus zwei Dingen braucht keinen
|
||||||
|
`LoadReport`-Typ — den gäbe es für genau eine Aufrufstelle.
|
||||||
|
|
||||||
|
Die `Result`-Kette endet **vor** `main`: `eprintln!("{e}")` plus `exit(1)`. `main() -> Result<…>`
|
||||||
|
druckt über `Debug` und setzt dem Spieler `LoadError { .. }` samt „Error: " vor.
|
||||||
|
|
||||||
|
### Panic-Grenze
|
||||||
|
|
||||||
|
Die Grenze ist die **Datenherkunft**, nicht die Schwere:
|
||||||
|
|
||||||
|
- Alles, was aus einer TOML kommt, ist `Result`.
|
||||||
|
- Alles, was aus dem eigenen Code kommt, darf panicken — Index in eine selbst gebaute `Vec`,
|
||||||
|
`unreachable!` im `match` über ein eigenes Enum. Ein Panic dort ist ein Engine-Bug und soll laut
|
||||||
|
sein; ihn in ein `Result` zu verpacken hieße, den Aufrufer nach etwas zu fragen, das er nicht
|
||||||
|
beantworten kann.
|
||||||
|
|
||||||
|
`unwrap()` auf Modder-Daten ist ausnahmslos falsch — genau das tut `items.rs::parse()` heute.
|
||||||
|
|
||||||
|
### Check
|
||||||
|
|
||||||
|
Ein `#[test]`, der `mods/core/` lädt und auf **null Warnungen** prüft. Damit fällt ein kaputtes
|
||||||
|
Kernspiel beim `cargo test` auf statt beim Spieler, und die Ladelogik bleibt frei von Sonderregeln.
|
||||||
|
Das ist die eine runnable Prüfung, die dieses Ticket hinterlässt.
|
||||||
|
|
||||||
|
### Auflagen an andere Tickets
|
||||||
|
|
||||||
|
- **11 UI-Gerüst und Screen-Modell**: Der Startbildschirm trägt eine Zeile mit Warnungsanzahl und
|
||||||
|
Logdateiname. Eine Zeile, kein Screen.
|
||||||
|
|
||||||
|
### Folgen für bestehenden Code
|
||||||
|
|
||||||
|
`items.rs::parse()` verliert sein `panic!`, `main()` bekommt den stderr-plus-exit-Pfad, die
|
||||||
|
`logs/`-Verwaltung kommt neu dazu.
|
||||||
|
|||||||
@@ -24,3 +24,11 @@ Entscheidungen darüber stehen noch aus:
|
|||||||
- **Welche Screens gibt es zum Start**, und welche Aktionen kennt `Game::apply(Action)`?
|
- **Welche Screens gibt es zum Start**, und welche Aktionen kennt `Game::apply(Action)`?
|
||||||
|
|
||||||
Ergebnis: Screen-Enum, Stack-Modell, Action-Liste und die Regel, was `game/` nicht importieren darf.
|
Ergebnis: Screen-Enum, Stack-Modell, Action-Liste und die Regel, was `game/` nicht importieren darf.
|
||||||
|
|
||||||
|
## Vorgaben aus anderen Tickets
|
||||||
|
|
||||||
|
- **02**: Charakterbogen und Charakterbau sind Registry-Schleifen, keine gemauerten Layouts —
|
||||||
|
Begründung in [Ticket 02](02-typ-registry.md).
|
||||||
|
- **04**: Der Startbildschirm trägt eine Zeile mit Warnungsanzahl und Logdateiname
|
||||||
|
(„12 Ladewarnungen — `logs/load-20260812-112305.log`"). **Eine Zeile, kein Screen** — der Inhalt
|
||||||
|
gehört in einen Texteditor, nicht in ein TUI mit Scrolling und Navigation.
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ auch über Ticket 13.
|
|||||||
- [01 — Effekt-Vokabular](issues/01-effekt-vokabular.md) ([#2](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/2)) — **Zwei Begriffe statt einem**: Modifikator (rechnet ein Attribut um, solange er existiert) und Aktion (ändert Zustand einmalig), getrennte Maschinen und getrennte TOML-Arrays. Fester Attributkatalog ohne Namensraum; `max_health` ist Attribut, der Lebensstand ist Zustand. Sechs Aktionen, `damage` eigenständig wegen der Minderungskette. Stapeln `(Basis + Σ flach) × (1 + Σ prozentual)`. Eine Uhr: der Zug. Sieben feste Schadenstypen, ein Minderungsmechanismus (`resistance`), `armor` nur Kurzschreibweise. Auren wohnen beim Träger. Bedingungsvokabular bewusst an Ticket 08 delegiert. Begriffe im [Glossar](../../CONTEXT.md).
|
- [01 — Effekt-Vokabular](issues/01-effekt-vokabular.md) ([#2](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/2)) — **Zwei Begriffe statt einem**: Modifikator (rechnet ein Attribut um, solange er existiert) und Aktion (ändert Zustand einmalig), getrennte Maschinen und getrennte TOML-Arrays. Fester Attributkatalog ohne Namensraum; `max_health` ist Attribut, der Lebensstand ist Zustand. Sechs Aktionen, `damage` eigenständig wegen der Minderungskette. Stapeln `(Basis + Σ flach) × (1 + Σ prozentual)`. Eine Uhr: der Zug. Sieben feste Schadenstypen, ein Minderungsmechanismus (`resistance`), `armor` nur Kurzschreibweise. Auren wohnen beim Träger. Bedingungsvokabular bewusst an Ticket 08 delegiert. Begriffe im [Glossar](../../CONTEXT.md).
|
||||||
- [02 — Typ-Registry statt Enums](issues/02-typ-registry.md) ([#3](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/3)) — **Es gibt keinen Item-Typ mehr**: `enum ItemType` vermischte Slot, Verhalten und Kategorie und entfällt ersatzlos zugunsten der Felder `slot`, `stackable`, `consumed_on_use`. `ItemWeapon` und `ItemArmor` fallen weg, ein Gegenstand ist einheitlich. **Fähigkeit** kommt als dritter Begriff neben Modifikator und Aktion dazu (Angebot statt Wirkung). Fünf moddbare Registries; Slots und sogar Charakteroption-Kategorien sind Inhalt, mit `pick = N`. Startausrüstung ist `on_create` als gewöhnliche Aktionsliste. `value` → `base_price`, `shop_price` → `price_modifier`.
|
- [02 — Typ-Registry statt Enums](issues/02-typ-registry.md) ([#3](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/3)) — **Es gibt keinen Item-Typ mehr**: `enum ItemType` vermischte Slot, Verhalten und Kategorie und entfällt ersatzlos zugunsten der Felder `slot`, `stackable`, `consumed_on_use`. `ItemWeapon` und `ItemArmor` fallen weg, ein Gegenstand ist einheitlich. **Fähigkeit** kommt als dritter Begriff neben Modifikator und Aktion dazu (Angebot statt Wirkung). Fünf moddbare Registries; Slots und sogar Charakteroption-Kategorien sind Inhalt, mit `pick = N`. Startausrüstung ist `on_create` als gewöhnliche Aktionsliste. `value` → `base_price`, `shop_price` → `price_modifier`.
|
||||||
- [03 — Mod-Ladepipeline und Manifest](issues/03-mod-ladepipeline.md) ([#4](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/4)) — Zwei Suchpfade: `mods/` neben der Binary (enthält `core/`) und `data_dir()/rustkeep/mods` über die Crate `directories`; bei gleicher ID gewinnt das Profil, mit Warnzeile. **`core` liegt als echter Mod-Ordner auf der Platte, `include_str!` entfällt.** `manifest.toml` mit vier Feldern (`id`, `name`, `version`, `depends`) — TOML statt YAML, weil `serde_yaml` und der Fork `serde_yml` beide archiviert sind. Ladereihenfolge aus einer selbstheilenden Spielerliste `load_order.toml`; `depends` sortiert nicht, sondern prüft, und eine Verletzung ist harter Startfehler. Jede `*.toml` im Mod wird eingelesen, jede darf jede Registry-Tabelle enthalten. Präfix wird automatisch vorangestellt, Überschreiben über `overrides = "core:iron_sword"` am Eintrag — der landet **nur** unter der überschriebenen ID. Drei Phasen (Manifeste → Einlesen → Auflösen), dadurch keine Vorwärtsreferenzen; Fehlereinheit ist der Eintrag. Kein Nachladen zur Laufzeit.
|
- [03 — Mod-Ladepipeline und Manifest](issues/03-mod-ladepipeline.md) ([#4](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/4)) — Zwei Suchpfade: `mods/` neben der Binary (enthält `core/`) und `data_dir()/rustkeep/mods` über die Crate `directories`; bei gleicher ID gewinnt das Profil, mit Warnzeile. **`core` liegt als echter Mod-Ordner auf der Platte, `include_str!` entfällt.** `manifest.toml` mit vier Feldern (`id`, `name`, `version`, `depends`) — TOML statt YAML, weil `serde_yaml` und der Fork `serde_yml` beide archiviert sind. Ladereihenfolge aus einer selbstheilenden Spielerliste `load_order.toml`; `depends` sortiert nicht, sondern prüft, und eine Verletzung ist harter Startfehler. Jede `*.toml` im Mod wird eingelesen, jede darf jede Registry-Tabelle enthalten. Präfix wird automatisch vorangestellt, Überschreiben über `overrides = "core:iron_sword"` am Eintrag — der landet **nur** unter der überschriebenen ID. Drei Phasen (Manifeste → Einlesen → Auflösen), dadurch keine Vorwärtsreferenzen; Fehlereinheit ist der Eintrag. Kein Nachladen zur Laufzeit.
|
||||||
|
- [04 — Fehlerstrategie für kaputte Mods](issues/04-fehlerstrategie.md) ([#5](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/5)) — **Syntax ist ein Datei-Fehler, alles Semantische ein Eintrags-Fehler**; damit 03s Versprechen hält, wird zweistufig deserialisiert (`Vec<Spanned<toml::Value>>`, dann Eintrag für Eintrag), sonst reißt ein Tippfehler die ganze `items.toml` mit. Genau drei harte Fehler: `depends` fehlend, `depends` verletzt, `core` nicht gefunden — stderr und `exit(1)`. `core` ist sonst kein Sonderfall; abgesichert wird es durch einen `#[test]`, der `mods/core/` mit null Warnungen lädt. Warnungen sammeln sich in `Vec<String>` und landen in `logs/load-<zeitstempel>.log` (letzte zehn), plus eine Zeile auf dem Startbildschirm — kein Report-Screen. Meldung `meinmod/items.toml:42: core:iron_sword — <message>`, Mod-ID vorne, keine Spalte. Rust: handgeschriebenes `enum LoadError` mit `impl Display` statt `thiserror`/`anyhow`, `load_mods() -> Result<(Registries, Vec<String>), LoadError>`, Kette endet vor `main` mit `eprintln!` und `exit(1)`. **Panic-Grenze ist die Datenherkunft**: TOML ist `Result`, eigener Code darf panicken.
|
||||||
- [05 — Research: Script-Engine](issues/05-research-script-engine.md) ([#6](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/6)) — Empfehlung Rhai wegen Fehlermeldungen mit Zeile und Spalte, billiger Typ-Brücke, AST-Hot-Reload und Build ohne `cc`. Gemessen: mlua liest unter `StdLib::ALL_SAFE` per `io.open` echte Dateien — „safe" heißt dort speichersicher, nicht fähigkeitsbeschränkt. Lua gewinnt bei der Modder-Vertrautheit; WASM scheidet für 1.0 aus. **Kernfolge: reden Scripts nur über das Effekt-Vokabular, ist die Engine-Wahl umkehrbar.** Entscheidung offen in Ticket 13.
|
- [05 — Research: Script-Engine](issues/05-research-script-engine.md) ([#6](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/6)) — Empfehlung Rhai wegen Fehlermeldungen mit Zeile und Spalte, billiger Typ-Brücke, AST-Hot-Reload und Build ohne `cc`. Gemessen: mlua liest unter `StdLib::ALL_SAFE` per `io.open` echte Dateien — „safe" heißt dort speichersicher, nicht fähigkeitsbeschränkt. Lua gewinnt bei der Modder-Vertrautheit; WASM scheidet für 1.0 aus. **Kernfolge: reden Scripts nur über das Effekt-Vokabular, ist die Engine-Wahl umkehrbar.** Entscheidung offen in Ticket 13.
|
||||||
- [06 — Research: ratatui-App-Architektur](issues/06-research-ratatui-architektur.md) ([#7](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/7)) — Screen-Stack über zentralem `Game`-State statt Elm oder Component-Trait; `game/` bleibt ratatui-frei; Popups per `Clear` auf Sub-`Rect` statt Widget-Stack; blockierendes `event::read()` ohne Tick; Dungeon-Grid ist Eigenbau, es gibt kein Widget dafür; ratatui 0.30.2 mit MSRV 1.88; `KeyEventKind::Press`-Filter unter Windows Pflicht. Voller Bericht in [research/06](research/06-ratatui-architektur.md).
|
- [06 — Research: ratatui-App-Architektur](issues/06-research-ratatui-architektur.md) ([#7](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/7)) — Screen-Stack über zentralem `Game`-State statt Elm oder Component-Trait; `game/` bleibt ratatui-frei; Popups per `Clear` auf Sub-`Rect` statt Widget-Stack; blockierendes `event::read()` ohne Tick; Dungeon-Grid ist Eigenbau, es gibt kein Widget dafür; ratatui 0.30.2 mit MSRV 1.88; `KeyEventKind::Press`-Filter unter Windows Pflicht. Voller Bericht in [research/06](research/06-ratatui-architektur.md).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user