7.5 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, oderanyhowmit Kontext, oder handgeschriebenesenum LoadError? Wo endet dieResult-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.
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.
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!immatchüber ein eigenes Enum. Ein Panic dort ist ein Engine-Bug und soll laut sein; ihn in einResultzu 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.