From 380400611e0110438b6e9ad1ec2db521c8449204 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Wed, 12 Aug 2026 13:00:50 +0200 Subject: [PATCH] docs: ticket 13 (script-engine) aufloesen, rhai gewaehlt, stufe c hinter 1.0, nachtrag an 01 --- .../issues/01-effekt-vokabular.md | 13 +- .../issues/13-script-engine-waehlen.md | 145 +++++++++++++++++- .scratch/rustkeep-1.0/map.md | 9 +- CONTEXT.md | 9 +- 4 files changed, 170 insertions(+), 6 deletions(-) diff --git a/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md b/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md index 34cb213..711c3d5 100644 --- a/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md +++ b/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md @@ -130,7 +130,18 @@ Eine Formel für alles: Basisangriff `scale = 1.0` · Feuerball `amount = 20` · `scale = 1.5` · Mischformen beides. Keine zwei Modi, kein Sonderfall für `core:attack`. Dazu **`flee` als achte Aktion** — Ticket 11 hat Flucht zu einer Fähigkeit gemacht, und die braucht -eine Aktion, die sie ausführt. Der Katalog hat damit acht Einträge. +eine Aktion, die sie ausführt. + +### Nachtrag aus Ticket 13: `run_script` + +`{ action = "run_script", script = "meinmod:feuerregen" }` als **neunte Aktion**. Ein Script ist damit +überall verwendbar, wo eine Aktion stehen darf, und braucht keine eigenen Platzierungsregeln. Es gibt +Aktionen zurück, die in die Warteschlange aus 08 wandern — der Schreibpfad bleibt also genau einer. + +### Der Katalog + +`damage` · `change_attribute` · `apply_modifier` · `remove_modifier` · `give_item` · `remove_item` · +`start_quest` (08) · `flee` (14) · `run_script` (13) — **neun**. ### Bewusst nicht entschieden diff --git a/.scratch/rustkeep-1.0/issues/13-script-engine-waehlen.md b/.scratch/rustkeep-1.0/issues/13-script-engine-waehlen.md index 88d1372..7051e2c 100644 --- a/.scratch/rustkeep-1.0/issues/13-script-engine-waehlen.md +++ b/.scratch/rustkeep-1.0/issues/13-script-engine-waehlen.md @@ -1,7 +1,7 @@ # 13 — Script-Engine wählen und Sandbox härten Type: grilling -Status: open +Status: resolved Blocked by: 01 ## Question @@ -27,3 +27,146 @@ reichen sie tiefer in den Game-State, ist sie es nicht. Zu klären: - **Wann fällt die Entscheidung endgültig** — jetzt, oder erst wenn Mod-Stufe (c) gebaut wird? Ergebnis: gewählte Engine mit Begründung, Härtungs-Checkliste, und die Regel, was Scripts sehen dürfen. + +## Answer + +Begriffe im [Glossar](../../../CONTEXT.md). + +### Die Wahl ist eine Implementierungsentscheidung — deshalb fällt sie leicht + +Scripts lesen über **eine Handvoll Funktionen** und schreiben ausschließlich über **Aktionen**. Weder +lesen sie direkt im `Game`-Baum noch halten sie Zustand außerhalb des Vokabulars. + +Damit ist die Schnittstelle klein genug, um sie in jeder Engine nachzubauen — genau die Leitplanke aus +Ticket 05. Ein Engine-Wechsel kostet die Bindungsschicht, nicht die Architektur. + +Die strikte Variante — Script bekommt Daten, gibt Aktionen zurück, liest gar nichts — wäre sauberer +gewesen und macht Scripts sinnlos: Was ein Script gegenüber einem Trigger *hinzufügt*, ist Rechnen +(„Schaden skaliert mit der Zahl der Untoten im Raum"), und dafür muss es lesen können. + +### Rhai — unbequem begründet + +**Für ein unveröffentlichtes Terminal-Roguelike schreibt niemand Mods.** Lua gewinnt bei der +Vertrautheit deutlich (Factorio, GMod, WoW, Noita, Don't Starve), aber das zahlt sich erst aus, wenn es +eine Modder-Gemeinde gibt — und die entsteht nicht durch die Sprachwahl, sondern durch ein Spiel, das +Leute spielen wollen. Bis dahin ist der Modder, den es zu bedienen gilt, der Autor selbst, und die +Charter nennt „Rust lernen" als halbe Motivation. + +Dazu der schärfste Einzelbefund aus 05: Ein Lua-Script hat unter `StdLib::ALL_SAFE` per `io.open` eine +echte Datei gelesen — „safe" heißt dort speichersicher, nicht fähigkeitsbeschränkt. Rhai hat Datei-, +Netz- und Prozesszugriff gar nicht erst in der Standard-Engine. + +**Mods gelten als vertrauenswürdig.** Kein Spiel dieser Größenordnung behandelt sie anders — Factorio, +Minecraft, Skyrim und Noita vertrauen alle. Der Spieler entpackt einen Ordner in sein Profil, dieselbe +Vertrauensentscheidung wie bei jeder heruntergeladenen Software; eine Sandbox schützt ihn nicht davor, +sondern verschiebt nur, wo er sie trifft. Damit scheidet Luau samt C++17-Toolchain und Lua-*Dialekt* +aus — der Dialekt, den die vertrauten Lua-Modder gerade nicht können. + +### Härtung — gegen Unfälle, nicht gegen Absicht + +```rust +engine.set_module_resolver(DummyModuleResolver::new()); // kein ./*.rhai von der Platte +engine.disable_symbol("eval"); // kein Code aus Strings +engine.set_max_operations(100_000); // CPU pro Aufruf +engine.set_max_call_levels(32); // Rekursion +engine.set_max_string_size(8_000); +engine.set_max_array_size(1_000); +engine.set_max_map_size(1_000); +engine.set_max_expr_depths(64, 32); // Parser-Tiefe +``` + +Alle Methodennamen gegen die Rhai-Doku geprüft. `on_progress` bleibt weg — es ist der Abbruchhaken für +lang laufende Scripts, und `set_max_operations` deckt denselben Fall mit einem Wert statt mit einer +Callback-Schleife ab. + +**`set_max_operations` gehört ausdrücklich *nicht* in `[balance]`** aus Ticket 14: Ein Mod, der sein +eigenes CPU-Limit hochsetzen kann, invertiert die Härtung. + +Ein Script, das an ein Limit stößt, ist ein **Eintrags-Fehler** nach der Regel aus 04: Warnzeile mit +Mod-ID, Script-Name und Zeile, Trigger verworfen, Spiel läuft weiter. Rhais Fehlermeldungen mit Zeile +*und* Spalte sind laut 05 genau dafür da. + +### Die Schnittstelle — was Scripts sehen dürfen + +``` +attribute(wer, name) -> f64 has_tag(wer, tag) -> bool +health(wer) -> f64 has_item(wer, id) -> i64 +stamina(wer) -> f64 floor() -> i64 +rand() -> f64 +``` + +Alle Leseoperationen sind an der Bedingungsgrammatik aus 08 gespiegelt — **Scripts sehen nichts, was +eine Bedingung nicht auch sähe**, sie können nur damit rechnen. Das ist die Regel; die Liste selbst +wird endgültig, wenn Stufe (c) gebaut wird. + +Bewusst gestrichen: `turn()` und `enemy_count()`. Für beide gibt es kein benanntes Beispiel, und +`enemy_count()` deckt sein eigenes Motivbeispiel („Schaden skaliert mit der Zahl der Untoten im Raum") +nicht einmal ab — dafür bräuchte es Iteration. Die Umkehrbarkeit aus Q1 hängt daran, dass diese +Schnittstelle klein bleibt; sie auf Vorrat wachsen zu lassen, während die Stufe verschoben ist, wäre +das Gegenteil. + +`health` und `stamina` sind **eigene** Funktionen statt `attribute("health")`, weil Ticket 01 scharf +trennt: `max_health` ist ein Attribut, der Lebensstand ist Zustand. Diese Trennung darf im Script-API +nicht verwaschen. + +**`rand()` läuft durch den Generator aus `Game`**, und Rhais eigenes `rand`-Paket wird nicht +registriert. Sonst gilt „Nachladen würfelt nicht neu" aus Ticket 14 für alles außer für Scripts — und +ausgerechnet die interessanten Effekte würfelten beim Laden neu. + +Geschrieben wird ausschließlich über Aktionen: Ein Script **gibt Aktionen zurück**, die in die +Warteschlange aus 08 wandern. Damit deckelt der 100-Events-Zähler auch ein Script, das sich selbst +aufruft. + +### Scripts sind die elfte Registry + +```toml +{ action = "run_script", script = "meinmod:feuerregen" } +``` + +`run_script` als **neunte Aktion**. Dadurch entstehen keine neuen Platzierungsregeln: Ein Script ist +überall verwendbar, wo eine Aktion stehen darf — an einem Gegenstand, einer Fähigkeit, einer +Questbelohnung, einem `ai`-Eintrag, einem Raum-Trigger. Dieselbe Bewegung wie `core:attack` in Ticket +11 und `flee` in 14: Was wie Engine-Logik aussieht, ist Inhalt. + +Die Alternative — Scripts registrieren sich selbst auf Events — wäre genau die Variante, bei der ein +Script Zustand außerhalb des Vokabulars hält, und die macht die Engine-Wahl unumkehrbar. + +### Risiken: beobachtet, mit benanntem Ausstiegssignal + +„Beobachten" ohne Kriterium ist ein Wort ohne Folge, deshalb ausgeschrieben: + +| Signal | Reaktion | +|---|---| +| Keine Rhai-Release in zwölf Monaten | wechseln — Stand 1.25.1 vom 29.05.2026, frisch, 3,4 Mio. Downloads in 90 Tagen | +| Unsoundness in einem Pfad, den wir tatsächlich nutzen | wechseln — Issue #894 ist das bisher nicht | +| `smartstring` unmaintained (RUSTSEC-2026-0249) | **kein** Ausstiegsgrund | + +Zu `smartstring`: Das Repo wurde am 03.05.2026 archiviert, die Advisory kam am 10.08.2026 und ist ein +INFO-Eintrag über fehlende Wartung, keine Verwundbarkeit (genannte Alternativen: `compact_str`, +`smol_str`). Rhai 1.25.1 erschien danach ohne Wechsel. `cargo audit` wird das melden, und das ist die +angemessene Menge Ärger dafür. + +### Stufe (c) wandert hinter 1.0 + +Die **Entscheidung** steht jetzt, die **Umsetzung** nicht. + +Stufen (a) und (b) sind durch 01–03 vollständig beschrieben und liefern bereits zehn Registries, +Trigger, Bedingungen, Affixe und `[balance]` — sehr viel Moddbarkeit ohne eine Zeile Script. Stufe (c) +ist die einzige, die eine neue Dependency, eine Bindungsschicht und eine Härtungsliste braucht. + +Ticket 02 hat die Regel selbst formuliert: „Braucht ein Gegenstand etwas, das Modifikator, Aktion oder +Fähigkeit nicht ausdrücken können, ist das eine Lücke im Vokabular." Eine Script-Engine einzubauen, +*bevor* jemand an diese Lücke gestoßen ist, baut die Antwort vor der Frage — und jede Lücke, die ein +Script stopfen würde, ist zuerst ein Hinweis, dass das Vokabular sie stopfen sollte. + +Die Entscheidung jetzt zu treffen kostet dagegen nichts und legt fest, **wogegen** das Vokabular gebaut +wird. Genau das ist die Leitplanke aus 05. + +**Folge: Die Destination der Map ändert sich** — 1.0 liefert Mod-Stufen (a) und (b), Stufe (c) kommt +danach. + +### Nachtrag an Ticket 01 + +`run_script` als **neunte Aktion**. Der Katalog hat damit: `damage`, `change_attribute`, +`apply_modifier`, `remove_modifier`, `give_item`, `remove_item`, `start_quest` (08), `flee` (14), +`run_script` (13). diff --git a/.scratch/rustkeep-1.0/map.md b/.scratch/rustkeep-1.0/map.md index 03a1a19..0d66c02 100644 --- a/.scratch/rustkeep-1.0/map.md +++ b/.scratch/rustkeep-1.0/map.md @@ -21,7 +21,11 @@ dort als Konvention im Body. Zuordnung Ticket → Gitea-Issue: Alle Architektur- und Design-Entscheidungen für rustkeep 1.0 sind getroffen — nicht gebaut, entschieden. 1.0 heißt: 5 Etagen mit Boss, Solo-Held mit Rasse und Klasse, Grid-Erkundung mit Menü-Kampf, -Inventar und Ausrüstung, Quests, NPCs, Handel, Speichern/Laden, Mod-Support auf drei Stufen. +Inventar und Ausrüstung, Quests, NPCs, Handel, Speichern/Laden, **Mod-Support auf den Stufen (a) und +(b)**. + +Stufe (c) — Scripting — ist entschieden, aber nicht Teil von 1.0: Engine (Rhai) und Schnittstelle +stehen, gebaut wird sie, wenn ein Modder an eine Lücke im Vokabular stößt (13). ## Notes @@ -44,7 +48,7 @@ Feststehende Rahmenentscheidungen aus der Charter-Session: | 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`), 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 | +| Mod-Stufen | Daten → Typen → Scripting, in dieser Reihenfolge. Scripting setzt auf der Typ-Registry auf. *13: Rhai gewählt, Umsetzung aber hinter 1.0 — gebaut wird sie, wenn das Vokabular an eine Grenze stößt* | | 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 | | Weltzustand | `Game`-Ownership-Baum, Verweise über `String`-IDs (keine Indizes, 07). `Registries` liegt daneben, nicht darin. Kein ECS | @@ -71,6 +75,7 @@ auch über Ticket 13. - [10 — Raum-Prefab-Format](issues/10-raum-prefab-format.md) ([#11](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/11)) — Freie Platzierung mit L-Korridoren; verbunden wird **Raum `i` mit Raum `i−1`**, weil eine Kette Erreichbarkeit *strukturell* garantiert statt sie nachtraeglich zu pruefen. Prefab als ASCII-Block mit Markern im Block, **Tueren explizit** (sonst stanzt der Generator Loecher in gezeichnete Waende); Rotation und Spiegelung erlaubt, `rotate = false` als Opt-out. **Ein `e` sagt „hier steht ein Gegner", nicht welcher** — das entscheidet die Etage, sonst waere jedes Prefab an eine Schwierigkeitsstufe gebunden. `[[prefabs]]` und `[[floors]]` sind die neunte und zehnte Registry; Auswahl durchgehend ueber **Tags**, Pflichtraeume zuerst platziert (das *ist* die Garantie), Flaechenpruefung beim Laden. **Kampf beginnt bei `room_entered`**, Gegner gehoeren zum Raum — das erspart Pfadsuche, Sichtlinien und Verfolgung. `Floor` mit `rooms`, `player_room: Option` (Korridor ist ein echter Zustand). **Siegbedingung erstmals definiert**: kein `[[floors]]` mit der naechsten Nummer = gewonnen, die Zahl „5" steht nirgends im Code. Nachtraege: `room_entered` an 08 (zwoelf Events), `GameOver { outcome }` an 14. - [11 — UI-Gerüst und Screen-Modell](issues/11-ui-geruest-screen-modell.md) ([#12](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/12)) — **„Aktion" hiess zwei Dinge**: `Action` bleibt das TOML-Effektvokabular aus 01, `Command` wird das UI-Kommando, Eingang `Game::execute(cmd, reg)` — die eine Stelle, an der ein Zug endet (Modifikatoren ticken, Event-Warteschlange leerarbeiten). Zwölf Commands, **alle kosten einen Zug** (ein `costs_turn`-Flag waere eine Ausnahmeliste); Angriff und Flucht sind keine Varianten, sondern `UseAbility("core:attack")` und `UseAbility("core:flee")` — damit moddbar. Ein gemeinsamer `Vec` fuer Screens und Popups; Screens **geben `Response` zurueck statt selbst zu schieben**, sonst haelt ein Screen ein `&mut` auf die `Vec`, in der er liegt. Ein `Response::Command` schliesst alle Overlays — dasselbe Flag, das schon fuers Rendern existiert. Zehn Screens, `ListState` in der Variante (der Stack erhaelt den Cursor gratis, solange der Screen liegt). Nur `KeyEventKind::Press`: bei `Repeat` bestimmte die Tastaturwiederholrate die Spielzeit; gegen zaehes Laufen hilft ein Renn-Kommando in `game/`. Tastenbelegung fest, Pfeile und `hjkl`. ratatui-Freiheit von `game/` prueft ein `#[test]` statt eines Workspace. Nachtrag an 01: `symbol` traegt das Zeichen und einen Farbnamen selbst — `ui/theme.rs` ist Code und damit fuer Modder unerreichbar. - [12 — Prototyp: ratatui-Durchstich](issues/12-prototyp-ratatui-durchstich.md) ([#13](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/13)) — **Das Modell aus 11 traegt.** Primaerquelle: Branch `prototyp/12-ratatui-durchstich`, `cargo run --example durchstich`. Bestaetigt: `mod game` bleibt ratatui-frei ohne Disziplin, Grid als eigenes `impl Widget` ueber `buf[(x,y)].set_symbol(…)`, `Vec` mit Overlay-Flag fuer Rendern *und* Auto-Schliessen, `Response` statt Stack-Mutation (kein Borrow-Aerger). **Der Windows-Fallstrick ist jetzt eine Zahl**: `Tasten 14 → Press 7 → Zuege 7` — ohne Filter kostete jeder Schritt zwei Zuege und damit doppelte Spielzeit. Zwei Korrekturen: 11 sagte „alle Commands kosten einen Zug", richtig ist **alle ausgefuehrten** (`Outcome { TurnPassed, Rejected(&str) }`, Grund reist mit zurueck); und `Command` muss `Clone` sein, damit `Confirm` es herausgeben kann. Ungetestet blieb `Registries` neben `Game` — der Durchstich laedt keine. +- [13 — Script-Engine wählen](issues/13-script-engine-waehlen.md) ([#14](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/14)) — **Rhai**, aber die Wahl ist dank der Leitplanke aus 05 nur noch eine *Implementierungs*-Entscheidung: Scripts lesen ueber eine Handvoll Funktionen (gespiegelt an der Bedingungsgrammatik aus 08 — sie sehen nichts, was eine Bedingung nicht auch saehe) und schreiben ausschliesslich ueber Aktionen. Begruendung unbequem, aber ehrlich — **fuer ein unveroeffentlichtes Terminal-Roguelike schreibt niemand Mods**, der Lua-Vorsprung zahlt sich erst mit einer Gemeinde aus. Mods gelten als **vertrauenswuerdig** (wie bei Factorio, Minecraft, Skyrim), gehaertet wird gegen Unfaelle: acht Engine-Schalter, gegen die Doku geprueft; `set_max_operations` ausdruecklich **nicht** in `[balance]`, sonst hebt ein Mod sein eigenes Limit auf. `rand()` laeuft durch den Seed aus 14, sonst wuerfelten ausgerechnet Scripts beim Laden neu. `run_script` als neunte Aktion, Scripts als **elfte Registry**. Risiken beobachtet mit benanntem Ausstiegssignal; `smartstring` ist ein INFO-Advisory, kein Ausstiegsgrund. **Stufe (c) wandert hinter 1.0** — eine Script-Engine zu bauen, bevor jemand an eine Vokabular-Luecke gestossen ist, baut die Antwort vor der Frage. - [14 — Kampfformeln und Rundenablauf](issues/14-kampfformeln-und-rundenablauf.md) ([#15](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/15)) — **Ein Zug = Spielerhandlung plus alle Gegnerreaktionen**, sonst tickte ein Debuff bei drei Gegnern viermal. Initiative als `i32` **am Wesen** statt als Reihenfolgeliste (die waere eine zweite Indexsammlung mit demselben Ungueltigkeitsproblem); tote Gegner erst am Ende von `execute`, nach dem Leeren der Warteschlange. Trefferformel und Schadenskette mit drei Klemmbereichen — `0.8` bei Resistenz, weil `1.0` Immunitaet und damit ein Spielabbruch waere. **Die Krit-Reihenfolge-Frage loest sich auf**: multiplikativ ist kommutativ. Eigener xorshift, **Seed im Save** — Nachladen wuerfelt nicht neu. Faehigkeiten kosten Ausdauer, Abklingzeit in Zuegen, gesperrt ⇒ `Rejected` ohne Zug. **Achte Registry: Wesen** — die Luecke lag seit 02 offen; `ai` als geordnete Bedingungsliste. `[balance]` verschmilzt **feldweise**, ausdrueckliche Ausnahme zu 03 (festes Schema). Vier Nachtraege: `scale` und `flee` an 01, drei Felder an 07, zwei Events an 08, `GameOver` an 11. ## Not yet specified diff --git a/CONTEXT.md b/CONTEXT.md index 6403cc7..ae62999 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -210,8 +210,8 @@ Speicherstand abgelehnt. ## Registry -Eine benannte Sammlung moddbarer Definitionen, geschlüsselt nach namespaced ID. Es gibt **zehn**, und -das hier ist die kanonische Liste — sie ist über fünf Tickets gewachsen und stand sonst nirgends +Eine benannte Sammlung moddbarer Definitionen, geschlüsselt nach namespaced ID. Es gibt **elf**, und +das hier ist die kanonische Liste — sie ist über sechs Tickets gewachsen und stand sonst nirgends vollständig: | Registry | TOML-Tabelle | @@ -226,12 +226,17 @@ vollständig: | [Wesen](#wesen) | `[[creatures]]` | | [Prefabs](#prefab) | `[[prefabs]]` | | [Etagen](#etage) | `[[floors]]` | +| Scripts | `[[scripts]]` | Jede dieser Tabellen darf in jeder beliebigen `*.toml` eines [Mods](#mod) stehen. Die Einstellungstabelle `[balance]` ist **keine** Registry: ein einzelner Block mit festem Schema, der feldweise verschmilzt, statt als ganzer Eintrag ersetzt zu werden. +`[[scripts]]` gehört zu Mod-Stufe (c) und ist entschieden, aber noch nicht gebaut — Scripts lesen über +neun Funktionen und schreiben ausschließlich über [Aktionen](#aktion), damit die Engine austauschbar +bleibt. + ## Mod Ein Ordner mit einer `manifest.toml` und beliebig vielen weiteren TOML-Dateien. Das Manifest gibt dem