153 lines
7.5 KiB
Markdown
153 lines
7.5 KiB
Markdown
# 04 — Fehlerstrategie für kaputte Mods
|
|
|
|
Type: grilling
|
|
Status: resolved
|
|
Blocked by: 03
|
|
|
|
## Question
|
|
|
|
Was passiert, wenn ein Mod kaputt ist — und wie sieht das im Rust-Code aus?
|
|
|
|
Explizites Rust-Lernticket. Heute macht `items.rs::parse()` ein `panic!` mit Backtrace; bei Mod-Support
|
|
ist fehlerhaftes TOML der Normalfall, nicht der Ausnahmefall. Zu klären:
|
|
|
|
- Welche Fehlerklassen gibt es? TOML-Syntax kaputt, Pflichtfeld fehlt, unbekannte Typ-ID,
|
|
Abhängigkeit nicht installiert, ID-Kollision ohne Override-Absicht.
|
|
- Welche davon sind hart (Start abbrechen) und welche weich (Mod überspringen, Rest laden, warnen)?
|
|
- Wie erreicht die Meldung den Spieler — vor dem TUI-Start auf stderr, oder als Bildschirm im TUI?
|
|
Braucht es ein Mod-Log?
|
|
- Rust-seitig: eigener Fehlertyp mit `thiserror`, oder `anyhow` mit Kontext, oder handgeschriebenes
|
|
`enum LoadError`? Wo endet die `Result`-Kette?
|
|
- Wie kommt die Herkunft in die Meldung — Datei, Zeile, Mod-ID?
|
|
|
|
Ergebnis: Fehlerklassifikation, Reaktion je Klasse, und die gewählte Rust-Fehlerbehandlung mit Begründung.
|
|
|
|
## Vorgabe aus Ticket 03
|
|
|
|
[Ticket 03](03-mod-ladepipeline.md) klassifiziert seine eigenen Fälle bereits durch: **hart** ist
|
|
allein `depends` fehlend oder in der Reihenfolge verletzt, alles Übrige ist Warnung plus Überspringen.
|
|
**Fehlereinheit ist der einzelne Eintrag**, nicht der Mod und nicht das Spiel.
|
|
|
|
Offen bleibt damit: kaputte TOML-Syntax (die Datei ist unlesbar, die Einheit „Eintrag" greift nicht),
|
|
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
|
|
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.
|
|
|
|
**Befund aus M1:** `Spanned` liefert den Anfang des *Eintrags* — also die `[[items]]`-Zeile —, nicht
|
|
die Zeile des schuldigen Feldes. Das deckt sich mit der Zusage oben („liefert dessen Zeilenbereich"),
|
|
sieht in der Praxis aber so aus:
|
|
|
|
```
|
|
test/items.toml:6: [1] — invalid type: integer `42`, expected a string
|
|
```
|
|
|
|
Zeile 6 ist der Eintragskopf, das kaputte Feld steht in Zeile 8. Den Feldnamen liefert `message()`,
|
|
weshalb die Meldung trotzdem eindeutig ist — die Genauigkeit reicht, solange ein Eintrag kurz bleibt.
|
|
|
|
`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.
|