Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rct-bridge — RCT1 LLM Guest Bridge

TL;DR (English): rct-bridge gives an LLM agent controlled read/write access to the memory of a running RollerCoaster Tycoon 1 process under Wine. A long-lived daemon owns the /proc/<pid>/mem handle and enforces memory windows, a field-level write mask and an undo journal; a thin MCP server exposes it to Claude Code or any other MCP client. The distilled reverse-engineering reference lives in PEEP_HANDBUCH.md. Docs are in German — the demo video shows the bridge in action.

Die Bridge gibt einem LLM (oder dir in der Shell) kontrollierten Lese- und Schreibzugriff auf den Speicher eines laufenden RCT.EXE unter Wine. Sie ist bewusst low-level: Der Agent rechnet Adressen selbst; die Bridge erzwingt nur die Speicherfenster, die Feld-Maske und das Undo-Journal. Die fachliche Referenz ist PEEP_HANDBUCH.md in diesem Repo (§2 Fenster, §4 Maske, §4.2 Namen, §11 M1-Rezept, §12 Smoke-Tests).

Demo-Video: Besucher-Happiness optimieren

Besucher-Happiness in RollerCoaster Tycoon optimieren zeigt die Bridge im Einsatz, mit Claude Code als Agent. Alle Schritte sind natürlichsprachliche Anweisungen, die der Agent über die MCP-Tools umsetzt:

  1. Die Parkbesucher werden in zwei Teams geteilt: Team Rot und Team Blau, jeweils komplett eingefärbt (Hemd und Hose). Team Rot bekommt Regenschirme, Team Blau werden sie weggenommen (snippets/outfit_split.py).
  2. Dann lässt der Agent es regnen (snippets/make_rain.py, Wetter-Regie über die globals_rw-Allowlist).
  3. Die blauen Besucher verlassen den Park (snippets/leave_park.py).
  4. Die Kleidung wird zur Happiness-Heatmap: Grün heißt sehr happy, über Gelb und Orange bis Dunkelrot; minütlich per Cronjob aktualisiert (snippets/happiness_heatmap.py, als Dauerschleife auch bridge/heatmap_loop.py).
  5. Trend-Auswertung: Werden die Besucher gerade glücklicher oder unglücklicher?
  6. Zum Schluss beobachtet der Agent die dreiköpfige Familie Mustermann minütlich, optimiert ihre Interaktion auf maximale Happiness und protokolliert Beobachtungen und Entscheidungen — das Ergebnis liegt als TAGEBUCH_MUSTERMANN.md in diesem Repo.

Was liegt wo

  • bridge/ — das Python-Paket: Daemon, CLI, MCP-Server, exec-Snippets, Tests.
  • PEEP_HANDBUCH.md — die destillierte Reverse-Engineering-Referenz (Offsets, Regeln, Rezepte, Smoke-Tests).
  • BNSkripte/ — Binary-Ninja-Skripte aus der RE-Arbeit, mit der das Handbuch entstanden ist.
  • TAGEBUCH_MUSTERMANN.md — Demo-Artefakt: das Tagebuch der Familie Mustermann aus dem Demo-Video oben.

Installation (Symlinks ins Spielverzeichnis)

Das Repo lebt außerhalb des Spielverzeichnisses; das Spiel bekommt nur Symlinks. bridge/config.py leitet das Spielverzeichnis aus dem Paketpfad ab, ohne Symlinks aufzulösen — deshalb funktioniert python3 -m bridge.daemon aus dem Spielverzeichnis unverändert.

  1. Repo klonen, z. B. nach ~/projects/rct-bridge.
  2. Im Spielverzeichnis (neben RCT.EXE) die zwei Pflicht-Symlinks anlegen: ln -s ~/projects/rct-bridge/bridge bridge und ln -s ~/projects/rct-bridge/PEEP_HANDBUCH.md PEEP_HANDBUCH.md — den Handbuch-Link braucht das MCP-Tool handbook, das die Datei im Spielverzeichnis sucht.
  3. Daemon, CLI und MCP-Server werden dann aus dem Spielverzeichnis gestartet, wie unten beschrieben.

Referenz-Setup hier: GOG-Version von RollerCoaster Tycoon Deluxe, über Bottles installiert; gestartet wird direkt mit System-Wine und gesetztem WINEPREFIX, gamescope übernimmt die Skalierung.

Architektur

Claude Code ─stdio/JSON-RPC─▶ MCP-Server ─Unix-Socket─▶ Daemon ─/proc/<pid>/mem─▶ RCT.EXE (Wine)
                              (dünner Adapter)          (Maske, Journal, exec-Sandbox)

Ein langlebiger Daemon startet wine RCT.EXE als Kindprozess und hält den /proc/<pid>/mem-Dateideskriptor. Das ist keine Geschmacksfrage, sondern die Ancestor-Falle: Bei kernel.yama.ptrace_scope=1 darf nur ein Vorfahr den Spielspeicher öffnen. Würde ein kurzlebiger CLI-Aufruf Wine spawnen und enden, käme der nächste Aufruf nicht mehr an den Prozess heran. Also führt der Daemon alle Zugriffe aus (Single Writer); Maske, Journal und exec-Sandbox existieren nur dort. Clients sprechen ein JSON-Zeilen-Protokoll über einen Unix-Socket; die CLI ist reines Shell-Sugar darüber, und der MCP-Server (bridge/mcp_server.py) ist nur ein weiterer dünner Client — er übersetzt MCP-Tool-Calls eins zu eins in dieses Protokoll.

Start

  1. Umgebung wie beim manuellen Spielstart vorbereiten und ins Spielverzeichnis wechseln — der Daemon vererbt seine Umgebung an das Spiel:

    export WINEPREFIX="/home/lutz/Downloads/rct_deluxe/RCT-Deluxe"
    cd "$WINEPREFIX/drive_c/GOG Games/RollerCoaster Tycoon Deluxe"
    
  2. Daemon starten. Einfachster Fall: python3 -m bridge.daemon (spawnt wine RCT.EXE direkt). Referenz-Setup mit gamescope (3072×2304, Nearest-Neighbor-Skalierung, ddraw-Override für die GOG-Version):

    python3 -m bridge.daemon --cmd 'gamescope -w 3072 -h 2304 -W 3072 -H 2304 -F nearest -- env WINEDLLOVERRIDES=ddraw=n,b wine RCT.EXE'
    

    Der Daemon startet den Befehl als Kindprozess und sucht das Spiel unter dessen Nachkommen (Daemon → gamescope → env → wine) — die Yama-Ancestor-Regel deckt beliebig tiefe Nachkommen ab, ein Launcher dazwischen ist also kein Problem. Gefunden ist das Spiel, sobald ein Nachkomme RCT.EXE an 0x400000 eingeblendet hat („MZ"-Check); danach lauscht der Daemon auf dem Socket (Default: $XDG_RUNTIME_DIR/rctbridge.sock).

  3. In einer zweiten Shell die CLI benutzen, z. B. python3 -m bridge.cli read globals ticks. Praktisch: alias rctbridge='python3 -m bridge.cli'.

  4. Beenden mit python3 -m bridge.cli shutdown — das beendet Launcher und Spiel (abschaltbar mit --leave-game beim Daemon-Start, aber Achtung: an einen verwaisten Prozess kommt ein neuer Daemon bei ptrace_scope=1 nicht mehr heran).

Varianten: --fake fährt den Daemon mit einem FakeMemory-Backend (zum Testen des Protokolls ohne Spiel), --attach PID hängt sich an eine laufende Instanz (braucht sudo sysctl kernel.yama.ptrace_scope=0).

Protokoll (eine JSON-Zeile pro Request und Response)

Op Beispiel-Request
read (Region) {"op":"read","region":"sprites","idx":17,"off":34,"size":2}
read (Global) {"op":"read","region":"globals","name":"ticks"}
write {"op":"write","region":"sprites","idx":17,"off":48,"data":"1c"}
write (Global) {"op":"write","region":"globals","name":"climate_rain_level","data":"01"}
exec {"op":"exec","code":"...","args":{"idx":17}}
rollback {"op":"rollback","n":1} oder {"op":"rollback","all":true}
status {"op":"status"}
shutdown {"op":"shutdown"}

Antworten sind {"ok":true, ...} oder {"ok":false,"error":"..."}. Bytes laufen als Hex-String. status liefert die Selbstauskunft: alle Regionen mit Basis/Stride/Modus und die Namen der lesbaren Globals.

Fenster und Maske

Die Regionen entsprechen Handbuch §2: sprites (RW mit Feld-Maske), strings (RW, Slot-Grenzen erzwungen), sprite_lists und rides (RO), globals (benannte Adressen, RO bis auf die Allowlist globals_rw — aktuell der Climate-Block fürs Wetter-Rezept, Handbuch §10; Writes nur in voller Breite des Globals). Die Feld-Maske ist eine Allowlist (§4): schreibbar sind nur Name-ID, Farben, Invalidate-Flag, peep_flags, Pathfind-Goal-Reset und time_lost — plus +0xC5/+0xC6 (Heading/Countdown), die zusätzlich nur bei belegtem Gast-Slot (peep_type == 0) beschreibbar sind, weil Staff dieselben Bytes als Order-Felder recycelt (§4.1). Stats, Position, State und Geld sind by design nicht schreibbar.

exec

Für Massen-Operationen (800 Gäste einzeln poken wäre Latenz-Tod) schickt man ein Python-Snippet, das im Daemon läuft. Es sieht genau vier Dinge:

  1. bridge mit read(region, idx, off, size), read_global(name), write(region, idx, off, bytes) und write_global(name, bytes) — Writes laufen durch Maske/Allowlist und Journal, die Sandbox öffnet keine Hintertür.
  2. struct für u16/u32-Gefummel.
  3. args — das JSON-Objekt aus dem Request (CLI: --args '{"idx":17}').
  4. log(...) für Zwischenmeldungen; das Ergebnis legt man in result.

Kein import, kein open, kein os. Schutzziel sind Unfälle, nicht Angreifer. Beispiele liegen in snippets/ (Gäste-Scan, Namens-Lookup, M1-goto, Wetter-Regie make_rain.py). Achtung: Ein Endlos-Loop im Snippet blockiert den Daemon — v0 hat bewusst keinen Timeout.

Wiederkehrende Sweeps gehören deshalb NICHT als Loop ins Snippet, sondern client-seitig: python3 -m bridge.heatmap_loop (S11) schickt alle 10 Sekunden den Sweep aus snippets/happiness_heatmap.py (Ampel-Farben nach Happiness, Schreiben nur bei Bucket-Wechsel) — Muster „Reflex-Schicht ohne LLM": frische Verbindung pro Request, der Daemon bleibt zwischen den Sweeps frei. Stoppen: Ctrl-C oder touch bridge/heatmap.stop.

Journal und Rollback

Jeder Write wird vor der Ausführung mit alten und neuen Bytes in eine JSONL-Datei geschrieben (Default bridge/journal.jsonl, Audit über Läufe hinweg) und auf einen Undo-Stack gelegt. rollback stellt LIFO die alten Bytes wieder her — der Stack gilt für die aktuelle Daemon-Laufzeit.

MCP-Server

python3 -m bridge.mcp_server exponiert den Daemon als MCP-Server über stdio (JSON-RPC 2.0, eine Nachricht pro Zeile; stdlib-only wie der Rest). Er ist ein reiner Adapter ohne eigene Logik: Jeder Tool-Call öffnet eine frische Socket-Verbindung zum Daemon — so hängt der sequenzielle Daemon nie in einer Dauerverbindung fest und behält zwischen zwei Requests Luft (dort laufen später die Watcher). Der Daemon muss also bereits laufen; der MCP-Server spawnt das Spiel bewusst nicht (Ancestor-Falle).

Registrieren in Claude Code (aus dem Spielverzeichnis):

claude mcp add rct-bridge -- python3 -m bridge.mcp_server

Abweichender Socket: claude mcp add rct-bridge -- python3 -m bridge.mcp_server --socket /pfad/zur.sock.

Die Tools entsprechen den Protokoll-Ops eins zu eins: read, read_global, write, write_global, exec, rollback, status — plus handbook, das im PEEP_HANDBUCH.md nachschlägt (ohne Argument das Inhaltsverzeichnis, mit section den ersten Abschnitt, dessen Überschrift den Suchtext enthält). shutdown ist absichtlich NICHT als Tool exponiert: Ein LLM-Client soll das Spiel nicht beenden können; das bleibt der CLI vorbehalten.

Ausblick Observability (beschlossen, kommt nach dem MCP-Server): ein watch-/poll_events-Tool im Daemon. In Claude Code wird der Aufruf als Background-Bash-Prozess gestartet, der endet, sobald das Event kommt — das weckt den Main Agent (bewusst Claude-Code-spezifisch). Wichtig fürs Design: Ein im Daemon blockierender Wait würde den sequenziellen Serve-Loop blockieren — entweder pollt das Skript selbst in einer Schleife, oder der Daemon bekommt ein accept-Timeout und prüft Bedingungen zwischen den Verbindungen.

Tests

python3 -m pytest bridge/tests/ -q — läuft komplett ohne Wine gegen FakeMemory (Adressrechnung, Maske inkl. Gate, Journal, Rollback, exec-Sandbox, Protokoll, MCP-Adapter inkl. Socket-E2E). Die Live-Verifikation am Spiel ist bewusst kein pytest: sie steht als nummeriertes Skript in bridge/SMOKE.md.

Grenzen von v0

Kein Watcher (register_watch/poll_events kommen später als weitere ops und MCP-Tools, s. o.), exec ohne Timeout, rides und die Item-/Ballon-Farben noch RO.

Lizenz

Der Code steht unter der MIT-Lizenz (siehe LICENSE). Die Doku-Artefakte PEEP_HANDBUCH.md und TAGEBUCH_MUSTERMANN.md stehen unter CC BY 4.0 — Weiterverwendung erlaubt, solange der Urheber genannt wird. Das Repo enthält keine Spieldateien; RollerCoaster Tycoon gehört seinen Rechteinhabern.

About

Controlled LLM read/write access to a running RollerCoaster Tycoon 1 process — MCP server, memory mask, undo journal

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages