diff --git a/.scratch/rustkeep-1.0/issues/03-mod-ladepipeline.md b/.scratch/rustkeep-1.0/issues/03-mod-ladepipeline.md index f84250e..bd71459 100644 --- a/.scratch/rustkeep-1.0/issues/03-mod-ladepipeline.md +++ b/.scratch/rustkeep-1.0/issues/03-mod-ladepipeline.md @@ -1,7 +1,7 @@ # 03 — Mod-Ladepipeline und Manifest Type: grilling -Status: open +Status: resolved Blocked by: 02 ## Question @@ -18,3 +18,146 @@ Das Kernspiel ist `core:` und damit selbst ein Mod. Zu klären: - 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. + +### 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`. diff --git a/.scratch/rustkeep-1.0/issues/04-fehlerstrategie.md b/.scratch/rustkeep-1.0/issues/04-fehlerstrategie.md index 00dc639..c99f871 100644 --- a/.scratch/rustkeep-1.0/issues/04-fehlerstrategie.md +++ b/.scratch/rustkeep-1.0/issues/04-fehlerstrategie.md @@ -21,3 +21,14 @@ ist fehlerhaftes TOML der Normalfall, nicht der Ausnahmefall. Zu klären: - 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. diff --git a/.scratch/rustkeep-1.0/issues/10-raum-prefab-format.md b/.scratch/rustkeep-1.0/issues/10-raum-prefab-format.md index aeb3ad1..e125e3e 100644 --- a/.scratch/rustkeep-1.0/issues/10-raum-prefab-format.md +++ b/.scratch/rustkeep-1.0/issues/10-raum-prefab-format.md @@ -20,3 +20,10 @@ Entschieden ist der Hybrid: Modder bauen Prefabs, der Generator setzt prozedural - Wie greift ein Mod ein — nur neue Prefabs hinzufügen, oder auch Generator-Parameter ändern? Ergebnis: Prefab-Schema mit Beispiel plus die Regeln, nach denen der Generator zusammensetzt. + +## Vorgabe aus Ticket 03 + +Prefabs sind eine **weitere Registry über dieselbe Ladepipeline**, kein eigener Lader — das +beantwortet die letzte Frage oben halb: Hinzufügen ist der Normalfall, Ersetzen geht über `overrides`. +Offen bleibt, ob **Generator-Parameter** (Etagen-Tabellen, Gewichtungen, Pflichträume) ebenfalls +Registry-Einträge sind — dann mit derselben Maschine moddbar — oder ein Sonderfall. diff --git a/.scratch/rustkeep-1.0/map.md b/.scratch/rustkeep-1.0/map.md index cf1123b..1d71565 100644 --- a/.scratch/rustkeep-1.0/map.md +++ b/.scratch/rustkeep-1.0/map.md @@ -43,7 +43,7 @@ Feststehende Rahmenentscheidungen aus der Charter-Session: | Rendering | ratatui + crossterm; `print_box` fliegt raus, sobald ratatui steht | | Etagen | Hybrid — Raum-Prefabs in TOML, prozedural zusammengesetzt | | Daten | Alles TOML, zur Laufzeit geladen. `include_str!` und `enum ItemType` fallen | -| Mod-IDs | Namespaced (`core:iron_sword`), Ladereihenfolge aus Mod-Manifest. Das Kernspiel ist selbst ein Mod | +| Mod-IDs | Namespaced (`core:iron_sword`), Präfix aus dem Manifest und automatisch vorangestellt. Ladereihenfolge aus einer Spielerliste, nicht aus dem Manifest (03). Das Kernspiel ist selbst ein Mod | | Mod-Stufen | Daten → Typen → Scripting, in dieser Reihenfolge. Scripting setzt auf der Typ-Registry auf | | Effekte | Ein generisches Effekt-System mit Wirkung auf Stats. Rasse, Klasse, Items, Statuseffekte, Quest-Belohnungen und später Scripts nutzen dasselbe Vokabular — dieses Vokabular *ist* die Mod-API | | Quests | Bedingung + Belohnung als Daten, generisches Event-/Trigger-System | @@ -61,6 +61,7 @@ auch über Ticket 13. - [01 — Effekt-Vokabular](issues/01-effekt-vokabular.md) ([#2](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/2)) — **Zwei Begriffe statt einem**: Modifikator (rechnet ein Attribut um, solange er existiert) und Aktion (ändert Zustand einmalig), getrennte Maschinen und getrennte TOML-Arrays. Fester Attributkatalog ohne Namensraum; `max_health` ist Attribut, der Lebensstand ist Zustand. Sechs Aktionen, `damage` eigenständig wegen der Minderungskette. Stapeln `(Basis + Σ flach) × (1 + Σ prozentual)`. Eine Uhr: der Zug. Sieben feste Schadenstypen, ein Minderungsmechanismus (`resistance`), `armor` nur Kurzschreibweise. Auren wohnen beim Träger. Bedingungsvokabular bewusst an Ticket 08 delegiert. Begriffe im [Glossar](../../CONTEXT.md). - [02 — Typ-Registry statt Enums](issues/02-typ-registry.md) ([#3](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/3)) — **Es gibt keinen Item-Typ mehr**: `enum ItemType` vermischte Slot, Verhalten und Kategorie und entfällt ersatzlos zugunsten der Felder `slot`, `stackable`, `consumed_on_use`. `ItemWeapon` und `ItemArmor` fallen weg, ein Gegenstand ist einheitlich. **Fähigkeit** kommt als dritter Begriff neben Modifikator und Aktion dazu (Angebot statt Wirkung). Fünf moddbare Registries; Slots und sogar Charakteroption-Kategorien sind Inhalt, mit `pick = N`. Startausrüstung ist `on_create` als gewöhnliche Aktionsliste. `value` → `base_price`, `shop_price` → `price_modifier`. +- [03 — Mod-Ladepipeline und Manifest](issues/03-mod-ladepipeline.md) ([#4](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/4)) — Zwei Suchpfade: `mods/` neben der Binary (enthält `core/`) und `data_dir()/rustkeep/mods` über die Crate `directories`; bei gleicher ID gewinnt das Profil, mit Warnzeile. **`core` liegt als echter Mod-Ordner auf der Platte, `include_str!` entfällt.** `manifest.toml` mit vier Feldern (`id`, `name`, `version`, `depends`) — TOML statt YAML, weil `serde_yaml` und der Fork `serde_yml` beide archiviert sind. Ladereihenfolge aus einer selbstheilenden Spielerliste `load_order.toml`; `depends` sortiert nicht, sondern prüft, und eine Verletzung ist harter Startfehler. Jede `*.toml` im Mod wird eingelesen, jede darf jede Registry-Tabelle enthalten. Präfix wird automatisch vorangestellt, Überschreiben über `overrides = "core:iron_sword"` am Eintrag — der landet **nur** unter der überschriebenen ID. Drei Phasen (Manifeste → Einlesen → Auflösen), dadurch keine Vorwärtsreferenzen; Fehlereinheit ist der Eintrag. Kein Nachladen zur Laufzeit. - [05 — Research: Script-Engine](issues/05-research-script-engine.md) ([#6](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/6)) — Empfehlung Rhai wegen Fehlermeldungen mit Zeile und Spalte, billiger Typ-Brücke, AST-Hot-Reload und Build ohne `cc`. Gemessen: mlua liest unter `StdLib::ALL_SAFE` per `io.open` echte Dateien — „safe" heißt dort speichersicher, nicht fähigkeitsbeschränkt. Lua gewinnt bei der Modder-Vertrautheit; WASM scheidet für 1.0 aus. **Kernfolge: reden Scripts nur über das Effekt-Vokabular, ist die Engine-Wahl umkehrbar.** Entscheidung offen in Ticket 13. - [06 — Research: ratatui-App-Architektur](issues/06-research-ratatui-architektur.md) ([#7](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/7)) — Screen-Stack über zentralem `Game`-State statt Elm oder Component-Trait; `game/` bleibt ratatui-frei; Popups per `Clear` auf Sub-`Rect` statt Widget-Stack; blockierendes `event::read()` ohne Tick; Dungeon-Grid ist Eigenbau, es gibt kein Widget dafür; ratatui 0.30.2 mit MSRV 1.88; `KeyEventKind::Press`-Filter unter Windows Pflicht. Voller Bericht in [research/06](research/06-ratatui-architektur.md). @@ -70,7 +71,7 @@ auch über Ticket 13. - **Boss-Design** — ob Bosse nur Gegner mit besseren Werten sind oder eigene Mechanik brauchen (Phasen, Skripte). Berührt Stufe (c) des Mod-Supports. - **NPC- und Handelsmodell** — Warenbestand, Preisbildung über `charisma`, Dialogdaten. Wartet auf das Event-System (08). - **Inventar-UX** — Slots, Gewicht oder Kapazität, Sortierung, Vergleichsansicht. Wartet jetzt auf das Screen-Modell (11), nicht mehr auf 06. -- **Mod-Verteilung** — wie Spieler an Mods kommen und ob es eine Versionsprüfung gibt. Wartet auf das Manifest-Format (03). +- **Mod-Verteilung** — durch 03 auf „Ordner ins Profil entpacken" geschrumpft, ohne Versionsprüfung. Offen ist nur noch, ob 1.0 mehr braucht. ## Out of scope diff --git a/CONTEXT.md b/CONTEXT.md index df86d49..a664c5a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -100,6 +100,16 @@ Die einzige Zeiteinheit des Spiels. Ein Feld weitergehen ist ein Zug, eine Kampf Es gibt keine getrennte Kampfrunden-Uhr: Befristete [Modifikatoren](#modifikator) ticken auch beim Erkunden, damit „ich warte den Debuff draußen ab" eine Entscheidung mit Kosten bleibt. +## Mod + +Ein Ordner mit einer `manifest.toml` und beliebig vielen weiteren TOML-Dateien. Das Manifest gibt dem +Mod seinen **Namensraum** — den Präfix vor dem Doppelpunkt, den der Lader jeder ID darin automatisch +voranstellt. + +Das Kernspiel ist ein Mod, kein Sonderfall: `core` liegt als gewöhnlicher Ordner auf der Platte und +wird über denselben Lader gelesen wie jeder Fremdmod. Genau das ist der Sinn — was nur für Fremdmods +gilt, ist nie getestet. + ## Effekt **Kein kanonischer Begriff.** Wird umgangssprachlich für [Modifikator](#modifikator) *und*