docs: ticket 13 (script-engine) aufloesen, rhai gewaehlt, stufe c hinter 1.0, nachtrag an 01
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user