# 03 — Mod-Ladepipeline und Manifest Type: grilling Status: resolved Blocked by: 02 ## Question Wie findet, ordnet und lädt das Spiel Mods — inklusive sich selbst? Das Kernspiel ist `core:` und damit selbst ein Mod. Zu klären: - Ordnerstruktur: Wo liegt `core`, wo liegen fremde Mods, was ist relativ zur Binary vs. zum Benutzerprofil? - Manifest: Welche Felder? ID-Namespace, Anzeigename, Version, Abhängigkeiten, Ladepriorität. - Ladereihenfolge: aus Abhängigkeiten berechnet oder vom Spieler festgelegt? Wie werden Zyklen behandelt? - Überschreiben: Wie sagt Mod B, dass er `core:iron_sword` ersetzt statt ergänzt? Ganz oder feldweise? - In welchen Phasen läuft das Laden — erst alle Typen, dann alle Instanzen? Was passiert bei Vorwärtsreferenzen? - Wird beim Start alles geladen, oder gibt es ein Nachladen zur Laufzeit? Ergebnis: Ordnerlayout, Manifest-Schema und die Phasen der Ladepipeline. ## Answer Begriffe im [Glossar](../../../CONTEXT.md). ### Ordnerlayout — zwei Suchpfade `mods/` neben der Binary ist die Auslieferung und enthält `core/`. `data_dir()/rustkeep/mods` im Benutzerprofil ist der Ort, an dem die Mods des Spielers eine Neuinstallation überleben. Der Pfad kommt aus der Crate `directories` (6.0.0) — acht handgerollte Zeilen hätten Windows und Linux bedient, `directories` nimmt macOS mit. Liegt dieselbe Mod-ID in beiden Pfaden, **gewinnt das Profil**, mit Warnzeile. Das ist die Richtung, in die der zweite Pfad zeigt: Er existiert, damit die Sachen des Spielers bleiben, und dann darf die Neuinstallation sie nicht stumm überstimmen. Es gilt auch für `core` — überschreibbar und protokolliert. Die Warnzeile ist die einzige Diagnose, die je zwischen „mein Mod tut nichts" und einer vergessenen alten Kopie im Profil unterscheidet. **`core` ist ein gewöhnlicher Mod-Ordner auf der Platte**, kein privilegierter Sonderfall. `include_str!` entfällt. Bliebe das Kernspiel einkompiliert, wäre die Mod-Fähigkeit nie getestet und jeder Bug im Lader träfe ausschließlich die Modder. `load_order.toml` liegt ebenfalls in `data_dir()`. Die XDG-Trennung von Daten und Konfiguration kostet einen zweiten Pfad, einen zweiten „existiert nicht"-Fall und einen Ordner, den der Spieler suchen muss — für eine einzelne Textdatei. ### Manifest — `manifest.toml`, Pflicht ```toml id = "meinmod" # Namensraum-Präfix und Mod-ID, unabhängig vom Ordnernamen name = "Mein Mod" version = "1.0.0" depends = ["core"] # nur Namen, keine Versionsbereiche ``` TOML, nicht YAML: `serde_yaml` ist seit dem 25.03.2024 archiviert und unmaintained, der Nachfolge-Fork `serde_yml` ebenfalls — nach einer Unsoundness im Serializer (RUSTSEC-2025-0068). Übrig bleiben junge Crates (`serde-saphyr`, `noyalib`, `yaml-rust2`). Für den Parser, der bei *jedem* Mod als erstes läuft, ist das eine schlechtere Basis als das ohnehin vorhandene `toml`. Dazu käme eine zweite Fehlermeldungssprache, die Ticket 04 mit abfangen müsste. YAMLs Stärke ist tiefe Verschachtelung; das Manifest hat vier flache Felder. `id` ist unabhängig vom Ordnernamen — das Manifest ist die Quelle, und Phase 1 liest ohnehin alle Manifeste, bevor sortiert wird. `depends` trägt keine Versionsbereiche: Semver-Matching ist ein Parser plus eine Konfliktauflösung für ein Problem, das erst existiert, wenn zwei Versionen desselben Mods im Umlauf sind. `version` ist bis dahin Anzeige und Save-Header. `description` und `author` kommen, wenn es einen Mod-Screen gibt, der sie anzeigt. Ein Ordner ohne Manifest ist kein Mod: Warnzeile, übersprungen, kein Fehler — sonst legt eine vergessene `screenshots/`-Kopie das Spiel lahm. ### Ladereihenfolge — Spielerliste, nicht Topo-Sort `load_order.toml` listet Manifest-IDs in Reihenfolge und ist selbstheilend: | Fall | Verhalten | |---|---| | Datei fehlt | wird erzeugt: `core` zuerst, Rest alphabetisch | | Ordner auf der Platte, nicht in der Liste | hinten angehängt, Datei zurückgeschrieben | | Eintrag in der Liste, kein Ordner | Warnzeile, übersprungen | Ein entpackter Mod läuft damit, ohne dass der Spieler von der Existenz der Sortierdatei weiß, und ein deinstallierter hindert das Spiel nicht am Start. Kein Sortier-Screen für 1.0 — die Datei *ist* die UI. **`depends` sortiert nicht, es prüft.** Fehlende Abhängigkeit oder falsche Position sind harte Startfehler im Klartext: „`meinmod` muss nach `magiemod` geladen werden, steht aber davor". Das ist der Preis dafür, dass ein Mensch die Reihenfolge pflegt — ohne die Prüfung merkt er den Fehler erst an einer Meldung über eine unbekannte ID, die auf die Symptomdatei zeigt statt auf die Ursache. Ein Mod, dessen Voraussetzung fehlt, produziert sonst zehn Folgefehler. ### Dateien im Mod Jede `*.toml` außer dem Manifest wird eingelesen, und jede Datei darf jede Registry-Tabelle enthalten (`[[items]]`, `[[slots]]`, `[[categories]]`, `[[options]]`, `[[abilities]]`). Keine festen Dateinamen — der Modder sortiert selbst, und ein großer Mod muss seine 4000 Gegenstände nicht in eine Datei zwingen. Das ist *weniger* Code, nicht mehr: eine Struct mit fünf optionalen `Vec`-Feldern, über jede gefundene Datei deserialisiert, Ergebnisse angehängt. Feste Namen bräuchten dieselbe Struct plus eine Namensliste plus Antworten auf „Datei fehlt" und „unbekannte Datei liegt daneben". ### IDs und Überschreiben Der Lader stellt `manifest.id` automatisch voran: `id = "schwert"` wird zu `meinmod:schwert`. Überschrieben wird über ein Feld am Eintrag: ```toml [[items]] id = "eisenschwert" overrides = "core:iron_sword" ``` Der Eintrag landet **nur** unter `core:iron_sword`; `meinmod:eisenschwert` wird nicht registriert. Der Zweck des Überschreibens ist, dass bestehende Verweise weiterfunktionieren — Loottabellen, `on_create`-Startausrüstung und Questbelohnungen zeigen auf `core:iron_sword` und liefern ab sofort das neue Schwert. Beide IDs zu registrieren hieße, denselben Gegenstand zweimal in der Welt zu haben: doppelt im Händlerbestand, doppelt in jeder Registry-Schleife. Zeigt `overrides` ins Leere — Zielmod nicht installiert oder vertippt —, gibt es eine Warnzeile und der Eintrag wird verworfen. Ihn ersatzweise unter der eigenen ID zu registrieren wäre schlimmer als nichts zu tun: Der Modder wollte etwas ersetzen und bekäme stumm einen zusätzlichen Gegenstand in die Welt gelegt. Überschreiben zwei Mods dasselbe, entscheidet die Ladereihenfolge — der letzte gewinnt. `overrides` ist ein Feld an *jedem* Registry-Eintrag, nicht nur an Gegenständen. **Befund aus M1: die Referenzseite war offen.** Oben steht, was mit der eigenen `id` passiert — nicht, was mit **Verweisen** passiert (`slot = "core:hand"`, `category = "core:race"`). Die wörtliche Fassung ist umgesetzt: Nur `id` wird präfixiert, Verweise stehen voll qualifiziert. Das ist asymmetrisch, und ein Modder wird darüber stolpern — er schreibt `id = "schwert"` und zwei Zeilen darunter `slot = "core:hand"`. Die Alternative wäre, dieselbe Regel auf jedes ID-förmige Feld anzuwenden: **kein Doppelpunkt heißt eigener Namensraum.** Das ist bequemer und macht mod-interne Verweise kürzer, verlangt aber, dass der Lader weiß, welche Felder IDs sind. Offen — zu entscheiden, bevor der erste Fremdmod existiert. ### Drei Phasen 1. **Manifeste** — alle Ordner beider Pfade scannen, `manifest.toml` lesen, Reihenfolge aus `load_order.toml` bestimmen, `depends` prüfen. 2. **Einlesen** — alle Dateien in Ladereihenfolge, Einträge nach ID in die Registries, spätere überschreiben frühere. Hier wird *nichts* aufgelöst. Dadurch gibt es keine Vorwärtsreferenzen: Ein Mod darf auf etwas zeigen, das erst ein späterer liefert. 3. **Auflösen** — alle Referenzen prüfen: Gegenstand → Slot, Charakteroption → Fähigkeit, Aktion → Gegenstand. **Fehlereinheit ist der einzelne Eintrag**, nicht der Mod und nicht das Spiel. Ein Tippfehler in einem von 200 Gegenständen darf die anderen 199 nicht mitreißen — beim Kernspiel wäre es der Totalausfall. Was genau eine kaputte Referenz auslöst, entscheidet Ticket 04. **Kein Nachladen zur Laufzeit.** Nachladen hieße, dass jede laufende Referenz auf einen Registry-Eintrag ungültig werden kann, mitten im Kampf — das ist kein Feature, sondern eine Invalidierungs-Architektur, die den `Game`-Baum aus Ticket 07 mitbestimmt. Neustart ist die Reload-Taste, und ein Terminalspiel startet in Millisekunden. ### Auflagen an andere Tickets - **04 Fehlerstrategie**: erbt den Katalog Warnung-vs-Abbruch. Die harten Fälle stehen fest — `depends` fehlend oder verletzt. Alles andere ist bislang Warnung plus Eintrag verwerfen; 04 muss das zu einer Regel machen und entscheiden, wo die Warnungen landen, wenn der Bildschirm ratatui gehört. - **09 Save-Schema**: Der Mod-Header nutzt `manifest.id` und `version`. Ohne Versionsbereiche in `depends` ist `version` dort das einzige Signal, dass ein Mod sich geändert hat — und damit 09 zugleich sein *einziger* Leser in 1.0. Fällt der Header, fällt das Feld mit. - **10 Raum-Prefab-Format**: Prefabs sind eine weitere Registry über dieselbe Pipeline, kein eigener Lader — sie erben Namensraum, `overrides` und die drei Phasen. ### Folgen für bestehenden Code `include_str!` und `load_items()` entfallen zugunsten des Mod-Laders. `mods/core/` entsteht, `weapons.toml` und `armors.toml` ziehen dorthin (verschmolzen laut Ticket 02). `directories` kommt als Dependency in `Cargo.toml`.