Files
rustkeep/.scratch/rustkeep-1.0/issues/04-fehlerstrategie.md

6.9 KiB

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 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.

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.

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.