174 lines
9.3 KiB
Markdown
174 lines
9.3 KiB
Markdown
# 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`.
|