Skip to content

Latest commit

 

History

History
487 lines (378 loc) · 47.5 KB

File metadata and controls

487 lines (378 loc) · 47.5 KB

PEEP_HANDBUCH — RCT1-Deluxe-Gästesteuerung für die LLM Guest Bridge

Stand: 2026-07-07 (Session 12: Wetter-Statemachine kartiert, Climate-Block RW via write_global → §10) · Binary: RCT.EXE (so heißt es auf Platte; die bndb heißt RCT_DELUXE.EXE.bndb). PE-Header verifiziert ✅ (S8): ImageBase 0x400000, RELOCS_STRIPPED → Ladeadresse garantiert, kein ASLR → alle Adressen statisch, gelten 1:1 in /proc/<pid>/mem. Zugriff ohne Root: Bridge startet Wine als Kindprozess (ptrace_scope=1, Ancestor-Regel) Autoritative Offset-Quelle: struct Sprite / struct PeepThought in RCT_DELUXE.EXE.bndb (Binary Ninja). Bei Widerspruch gewinnt die BN-Struct. Doppelrolle: LLM-Manual (der Agent rechnet Adressen selbst) UND Spezifikation der Python-Bridge v0 (Fenster, Maske, Watcher).

Legende: ✅ verifiziert (Decompilat/Asm) · ⚠️ Hypothese (Quelle angegeben) · RW in der Bridge schreibbar · RO lesbar · RO! bewusst gesperrt (Design) · TODO(#N) = offener Punkt, Nummer aus handoff.md


1. Leitplanken (nicht verhandelbar)

Drei Schichten, nur die mittlere wird ersetzt:

  1. Bedürfnis-Simulation (Happiness/Energy/Nausea/Hunger…) = unangetastet. Stats sind Reward-Funktion, keine Schreibziele.
  2. Entscheidung = LLM setzt Intentionen: Ziel-Ride über den Heading-Mechanismus (§5). Stände/WCs sind intern Rides.
  3. Ausführung = Chris Sawyers Pathfinding. Kein Schritt-für-Schritt-Steering, kein Teleport, kein Geld-Auffüllen.

Decision-Loop des Agenten: nur handeln, wenn der Gast walking/idle und ohne Ziel ist; action ∈ {goto_ride, idle, leave_park}. Die Feld-Maske (§4) erzwingt das technisch.


2. Speicherfenster der Bridge

Adressierung ist region-relativ; das LLM rechnet idx*stride+offset selbst, kann aber nie außerhalb des Fensters schreiben.

Region Bereich Layout Modus
sprites 0x963d70 – 0xa9c570 5000 × 0x100 RW mit Feld-Maske (§4)
sprite_lists 0xa9c570 … Heads/Counts (§3) RO — Linked Lists nie anfassen
strings 0xa9f5f8 – 0xaa75f8 1024 × 0x20 RW — Custom-Namens-Slots (§4.2); reine Byte-Slots, keine Verkettung
rides 0xaa75fc – 0xacd39c 255 × 0x260 RO (v1)
globals Einzeladressen (§10) RO + RW-Allowlist globals_rw (S12: der Climate-Block — Wetter ist Regie-Zustand, keine Bedürfnis-Simulation; Rezept §10)

Peep-Adresse: 0x963d70 + idx*0x100 + offset, idx ∈ [0, 4999] Ride-Adresse: 0xaa75fc + ride_id*0x260 + offset, ride_id ∈ [0, 254]

Alle Writes laufen durchs Undo-Journal (alte Bytes loggen, Rollback möglich). Das exec-Primitive bekommt nur bridge.read/write/read_global/write_global injiziert (kein os/ctypes).


3. Sprite-Pool-Grundlagen

  • Das „Peep-Array“ ist der Sprite-Pool (alle beweglichen Entities); Peep = eine Sprite-Sorte. ✅
  • Freier Slot: sprite_identifier (+0x00) == 0xff. Free-List = LIFO-Stack. ✅
  • 6 doppelt verkettete Listen, angewählt über linked_list_offset (+0x08) als Byte-Offset in Heads 0xa9c570 / Counts 0xa9c57c. Offset 4 = Peep-Liste (Head g_sprite_list_head_peeps 0xa9c574); counts[0] = frei; 0xa9c582 = Misc-Count. ✅
  • Die Peep-Liste ist nach formatiertem Namen sortiert ✅ (S8): mcp_reinsert_peep_in_name_sorted_list 0x435b05 hängt einen Peep nach jedem Namenswechsel aus und fügt ihn per String-Vergleich an der richtigen Stelle wieder ein. Ein Bridge-Write auf +0x22 lässt die Verkettung intakt, nur die Sortierung wird stale (kosmetisch, betrifft das Gästelisten-Fenster).
  • Stale-Index-Gefahr: sprite_index hat keinen Generation-Counter — Bridge muss vor jedem Zugriff prüfen, dass der Slot noch ein Gast ist (sprite_identifier != 0xff und peep_type == 0).
  • Spatial-Index g_sprite_spatial_index 0xc2d4f6 (128×128-Buckets, Off-Map-Bucket 0xc354f6), verkettet über next_in_quadrant (+0x02). Nur RO-Hintergrundwissen.

4. struct Sprite — Feldtabelle mit Zugriffsmaske

Write-Maske ist eine Allowlist: nur die als RW markierten Offsets sind schreibbar, +0xC5/+0xC6 zusätzlich nur wenn peep_type (+0x2E) == 0 (§4.1). Alles andere: lesen ja, schreiben nein.

Offset Typ Name (BN) Zugriff Semantik / Belege
0x00 u8 sprite_identifier RO 0xff = freier Slot ✅
0x01 u8 misc_identifier RO Subtyp für Misc-Sprites
0x02 u16 next_in_quadrant RO Spatial-Index-Kette — nie schreiben
0x04 u16 next RO Linked List — nie schreiben
0x06 u16 previous RO Linked List — nie schreiben
0x08 u8 linked_list_offset RO 4 = Peep-Liste (§3) ✅
0x09 u8 sprite_height_negative RO
0x0A u16 sprite_index RO eigener Index (kein Generation-Counter!)
0x0C u16 sprite_flags RO
0x0E/0x10/0x12 i16×3 x / y / z RO! Position — kein Teleport by design
0x14–0x1E sprite_width/heights/bbox/direction RO Render-Geometrie
0x22 u16 name_string_idx RW Name-String-ID, vollständig dekodiert ✅ (S8) → §4.2 (Lese- und Schreibrezept). RW nur zusammen mit dem Pool-Rezept — nie auf eine ID zeigen lassen, deren Slot leer ist
0x2B u8 state RO! RCT2-Statenummern (Jump-Table 0x78ef3c): 0 Falling, 2 QueuingFront, 3 OnRide ✅, 4 LeavingRide, 5 Walking ✅, 6 Queuing, 7 EnteringRide ✅, 8 Sitting, 9 Picked ✅, 17 Buying ✅ (Rest aus RCT2-Enum ⚠️). 3/7 verifiziert S6 via decrement_num_riders (Fahrer-Zähler); Needs-Setter-Guard „5 oder 8“ = Walking/Sitting. Kein State-Forcing.
0x2C u8 sub_state RO
0x2D u8 sprite_type RO Erscheinungsbild/Animation; Updater sub_4379ec ⚠️TODO(#6). 18 = Schirm aufgespannt ✅ (S13): Engine setzt den Wert selbst, sobald rain_level ≠ 0 und Item-Bit 4 gesetzt ist — bei Gehenden, Anstehenden und Sitzenden (states 5/6/2/7/8), NICHT auf dem Ride (state 3 bleibt beim alten Typ); überschreibt dabei auch Food-Sprite-Typen (14/15/22/24/26 → 18)
0x2E u8 peep_type RO 0 = Gast, 1 = Staff — Gate für +0xC5/C6! ✅ (§4.1)
0x2F u8 no_of_rides RO auch Stationswahl-Parität bei Doppelstation (§5.2)
0x30 u8 tshirt_colour RW ✅ live (S9): Write färbt sofort um, kein Invalidate nötig. Farb-Enum ≠ RCT2-Reihenfolge; RCT1-Tabelle in §4.3 (aus OpenRCT2, Live-Check offen)
0x31 u8 trousers_colour RW ✅ live (S9), wie +0x30
0x38 u8 energy RO! Stat-Paar-Mechanik §7; Walking-Check energy<=0x32
0x39 u8 energy_target RO!
0x3A u8 happiness RO! Walking-Check <0x80
0x3B u8 happiness_target RO! ✅ Lost-Trio-Penalty −30, geklemmt auf 0
0x3C u8 nausea RO! Walking-Check >0xaa
0x3D u8 nausea_target RO!
0x3E u8 hunger RO! ✅ (S6) niedrig = hungrig. Gedanke 0x14 bei ≤ 10 (unterdrückt solange Food-Items, Maske 0x36ba3e0); Decay −2/Tick128; Essen +7 (clamp 0xff); flags&0x800 → −15 extra. Vehicle-Union: bei Zug-Sprites ist +0x3E next_vehicle_in_train ⚠️ — Queue-Ketten in update_peep laufen über Vehicles, nicht Peeps
0x3F u8 thirst RO! ✅ (S6) Gedanke 0x15 bei ≤ 25; Decay −4/Tick128, −1 extra bei Temperatur ≥ 0x15; Trinken +7; Essen −3
0x40 u8 toilet RO! ✅ (S6) Essen +2 (clamp 0xff); WC-Gedanke 0x16
0x41 u8 pad_41 RCT2: mass ⚠️ unverifiziert
0x42 u8 time_to_consume RO ✅ (S6) −3/Tick128, pausiert auf dem Ride (state 3 = OnRide — auf der Bahn wird nicht gegessen); bei 0: Item-Bit weg, Leergut aus g_item_empty_container_map 0x78f114
0x43 u8 intensity_prefs RO! ✅ (S7) Nibbles: min = (low&0xF)·100 − happiness, max = min(high·100, 1000) + happiness; should_go_on_ride vergleicht gegen ride->intensity (Gedanken 4/5)
0x44 u8 nausea_tolerance RO! ✅ (S7) &3 = Index in g_nausea_tolerance_thresholds 0x78f130 ({u16,u16}×4); Max+happiness vs. ride->nausea → Gedanke 7
0x45 u8 window_invalidate_flags RW UI-Refresh nach Poke (§9)
0x46 u8[0x22] pad_46 unkartiert
0x68 u8 current_ride RO bei Ankunft/Buying gesetzt ✅
0x6D/0x6E/0x70 u8 field_6d/6e/70 unbekannt
0x71 u8 action RO 0x18 = Winken ✅ (S11: taucht nur bei Gästen mit Flag-Bit 0x10 auf, 15-Test-gegen-15-Kontroll-Vergleich), 0x19 = Foto ✅; 0xfe/0xff = keine Action (beide beobachtet S11)
0x79 u8 interaction_ride_index RO
0x7A u16 time_in_queue RO
0x7C u8[0x20] rides_been_on RO 256-Bit-Maske, Bit = Ride-ID ✅
0x9C u32 name_args RO
0xA0 u32 cash_in_pocket RO! kein Geld-Cheat by design
0xA8 u32 time_in_park RO Tick beim Eintritt
0xAD u8 previous_ride RO 0xff = keins; Kurzzeitgedächtnis ✅
0xAE u16 previous_ride_time_out RO 720 Ticks ≈ 29 s Re-Choice-Sperre ✅
0xB0 PeepThought[5] thoughts RO §8 — Diagnose-Kanal des LLM
0xC4 u8 path_check_counter RO check_for_path alle 16 Ticks ✅
0xC5 u8 guest_heading_to_ride_id RW·Gate DAS Intent-Feld. 0xff = kein Ziel ✅
0xC6 u8 peep_is_lost_countdown RW·Gate ~1 Dekrement pro gelaufener Kachel; §6 ✅
0xC7 u8 photo1_ride_ref RO Demolish cleart unter Item-Bit 3 ✅
0xC8 u32 peep_flags RW Bit0 = LEAVING_PARK ✅; 0x20 = HAS_PAID_FOR_PARK_ENTRY ✅ (S7: viertelt die Zahlungsbereitschaft im Value-Check); 0x400 erzwingt Lost-Gedanken ✅; 0x800 = schneller hungrig (RCT2 PEEP_FLAGS_HUNGER) ✅ (S6); Easter-Egg-Bit 0x10 wave ✅ (S11: Engine animiert selbst, Action +0x71 zeigt 0x18 nur bei geflaggten Gästen; parkweit gesetzt, kein Invalidate nötig); 0x40 photo / 0x80 paint ⚠️ (einzeln testen, §12); Bit 19 im Favourite-Kontext ⚠️
0xCC/CD/CE u8×3 pathfind_goal_x/y/z RW Goal-Cache; Reset = dword @+0xCC := 0xFFFFFFFF ✅
0xCF u8 pathfind_history_idx RW Ring-Index (Teil des Reset-dwords)
0xD0 {x,y,z,mask}[4] pathfind_history RO ✅ (S6) letzte 4 Kreuzungen mit tried_dir_mask = dort schon probierte Ausgänge (§5.3); Reset bei neuem Goal
0xE0 u8[0xC] pad_e0 +0xE2 Zeit-auf-Ride? ⚠️
0xEC/ED/EE u8×3 no_of_food/drinks/souvenirs RO
0xF0 u8 voucher_type RO 1 = RIDE_FREE (arg = Ride), 3 = FOOD (String 0x7b9+arg); Anzeige-String 0xb2b+type ✅
0xF1 u8 voucher_arguments RO
0xF4 u8 time_lost RW im M1-Rezept := 0; Sättigung 254→220 + Gedanke 0x10 ✅
0xF6 u8 balloon_colour RO (v1)
0xF7 u8 umbrella_colour RW ✅ live (S13): Schirmfarbe nach §4.3-Enum; zusammen mit Item-Bit 4 (+0xFC) setzen, aufspannen macht die Engine bei Regen selbst (siehe +0x2D)
0xF8 u8 hat_colour RO (v1)
0xF9 u8 favourite_ride RO 0xff = keins; Demolish setzt 0xff ✅
0xFC u32 item_standard_flags RW·Byte 0 OpenRCT2-Bitwerte ✅: 0 Balloon, 2 Map, 3 Photo, 4 Umbrella, 14 Voucher. Seit S13 ist Byte 0 in der Maske (Schirm-Verteilung: Bit 4 per Read-Modify-Write setzen, Farbe an +0xF7); Bytes 1–3 bleiben RO. Schirm-Entzug wirkt, ist bei Regen aber NICHT permanent ✅ (S13b): Gäste kaufen am Kiosk nach (29 Rückkäufe in ~1 min); sprite_type 18 tritt nie ohne gesetztes Bit 4 auf (29/29-Gegenprobe). Anomalie: pick_ride prüft &2 statt Map &4 — RCT1-Bug oder Fehldeutung? (§13 b)

4.1 Staff-Union — das peep_type-Gate

Staff recycelt +0xC5/+0xC6 als eigene Felder (Orders): sub_44f645 schreibt Defaults (C5=bl, C6∈{0,0xf,3}), sub_44f89b testet Order-Bits 1/2/4/8 = Handyman mähen/gießen/leeren/fegen. ✅ Konsequenz: Bridge interpretiert/schreibt +0xC5/C6 als Heading nur bei peep_type==0. Umbenennung der Staff-Funktionen: TODO(#8).

4.2 Namen & Custom-String-Pool (✅ S8, TODO(#3) gelöst)

Der Pool: g_custom_string_pool 0xa9f5f8, 1024 Slots à 32 Bytes (Ende 0xaa75f8 = direkt vor ticks). Jeder Slot ist ein nackter C-String, maximal 31 Zeichen + NUL. Ein Slot ist frei, wenn sein erstes Byte 0 ist. Keine Verkettung, keine Refcounts — nur Bytes. Der Pool ist gemeinsam für Gast-, Ride-, Park-Namen und Banner-Texte: 1024 Custom-Namen insgesamt.

String-ID ↔ Slot: id = 0x8000 + bank·0x200 + slot (u16), Rückrechnung slot = id & 0x3ff. Custom-IDs sind genau der Bereich 0x8000 ≤ id < 0x9000. Gast/Ride/Park benutzen Bank 4 → IDs 0x8800 + slot; Banner Bank 0 (IDs 0x8000 + slot). Die Bank ist reines Tagging — Freigabe und Anzeige maskieren immer mit & 0x3ff.

Lese-Rezept „Wie heißt Gast idx?“ (gilt analog für ride+0x22 und den Park über Global 0xa9c588):

  1. id = u16 an peep+0x22 lesen.
  2. Wenn 0x8000 ≤ id < 0x9000: Custom-Name, Text = C-String an 0xa9f5f8 + (id & 0x3ff)·0x20. Fertig.
  3. Wenn 0xa000 ≤ id < 0xe000: „echter Gästename“ (Option Show real guest names). Vorname = String hinter Pointer g_real_name_firstname_ptrs 0x79570c[(id − 0xa000) & 0x3ff] (1024 Vornamen), dann Leerzeichen, dann Nachnamen-Initial "BCDFGHJKLMNPRSTWs"[(id − 0xa000) >> 10 & 0xf] plus Punkt — ergibt z. B. „John B.“.
  4. Sonst (id < 0x8000): Standard-String wie „Guest {Nr}“ aus der Stringtabelle (Pointer-Array g_string_table_ptrs 0x8a220c[id]); die Gastnummer steckt in name_args +0x9C.

Beweis: mcp_format_string_id_to_buffer 0x45272e implementiert exakt diese vier Zweige.

Schreib-Rezept M2 „Gast idx heißt jetzt Claude“:

  1. Freien Slot suchen: s mit Byte 0 == 0 an 0xa9f5f8 + s·0x20 (linear scannen, wie das Original).
  2. Text (≤ 31 Zeichen + NUL) in den Slot schreiben.
  3. Alte ID an peep+0x22 merken.
  4. u16 peep+0x22 = 0x8800 + s schreiben.
  5. War die alte ID selbst custom (0x8000 ≤ alt < 0x9000): alten Slot freigeben (Byte 0 = 0) — sonst leakt der Pool.
  6. Optional UI-Invalidate (§9). Bewusst weggelassen: das Reinsort in die namenssortierte Peep-Liste (§3, kosmetisch) und die Easter-Egg-Reevaluation (Namen wie „Claude“ triggern ohnehin keine).

Duplikate sind auf Byte-Ebene erlaubt (das native UI verweigert sie nur per Vorab-Check; Banner registrieren Duplikate regulär). Für „alle namens Claude“ ist genau das erwünscht. Warnung: Nie +0x22 auf eine Custom-ID setzen, deren Slot leer ist — die Anzeige liest dann einen Leerstring.

Filter „alle namens Claude“ (billige Richtung): Erst die ≤1024 Pool-Slots nach dem Text durchsuchen (Treffer-Slots → IDs 0x8800+s), dann die Peep-Liste einmal durchgehen und Gäste mit passender +0x22 einsammeln — statt für jeden Gast den Namen zu formatieren.

Easter-Egg-Namen (Re-Evaluation in set_guest_name, Matcher-Tabelle 0x793a74, Indizes == OpenRCT2-Reihenfolge): 4 CHRIS SAWYER→0x40 Foto · 5 KATIE BRAYSHAW→0x10 Winken · 6 MELANIE WARN→Stats-Boost (Happiness 250, Energy 127, Nausea 0) · 7 SIMON FOSTER→0x80 Malen · 8 JOHN WARDLEY→0x100 „Wow!“ · 9 LISA STIRLING→0x200 Litter · 0xa DONALD MACRAE→0x400 Lost · 0xb KATHERINE MCGOWAN→0x800 Hunger · 0xc FRANCES MCGOWAN→0x1000 WC · 0xd CORINA MASSOURA→0x2000 Crowded · 0xe CAROL YOUNG→0x4000 Happiness-Sink · 0xf MIA SHERIDAN→0x8000 Nausea · 0x10 KATIE RODGER→Bit 0x1 (⚠️ Divergenz: OpenRCT2 setzt SLOW_WALK Bit 0x2; Bit 0 ist in RCT1 sonst LEAVING_PARK — empirisch testen) · 0x11 EMMA GARRELL→0x10000 Purple · 0x12 JOANNE BARTON→0x20000 Pizza · 0x13 FELICITY ANDERSON→0x40000 Explode. Indizes 0–3 (Schumacher/Villeneuve/Hill/Mr Bean) sind die Go-Kart-Eggs und werden ride-seitig geprüft, nicht hier.

4.3 RCT1-Farb-Enum für +0x30/31 (S10, aus OpenRCT2 — Live-Check offen)

Quelle: OpenRCT2-SV4-Import, src/openrct2/rct1/Tables.cpp, RCT1::GetColour (Stand develop, 2026-07-06). Die Tabelle konvertiert dort RCT1-Farbwerte nach RCT2; rückwärts gelesen ist sie genau unser Wert→Farbe-Mapping. Sie erklärt die S9-Beobachtung: Wert 18 ist in RCT1 lightBrown (bräunlich) — im RCT2-Enum wäre 18 YELLOW. Das Enum gilt nicht nur für Kleidung, sondern auch für Ride- und Ballon-Farben.

Wert Farbe Wert Farbe
0 black 16 lightOrange
1 grey 17 darkOrange
2 white 18 lightBrown
3 lightPurple 19 saturatedBrown
4 brightPurple 20 darkBrown
5 darkBlue 21 salmonPink
6 lightBlue 22 bordeauxRed
7 darkWater 23 saturatedRed
8 saturatedGreen 24 brightRed
9 darkGreen 25 brightPink
10 mossGreen 26 lightPink
11 brightGreen 27 darkPink
12 oliveGreen 28 darkPurple
13 darkOliveGreen 29 lightWater
14 yellow 30 brightYellow
15 darkYellow 31 icyBlue

Werte ≥ 32 sind undefiniert (OpenRCT2 fängt sie mit black ab; was das echte RCT1 rendert, ist unbekannt — nicht schreiben).

Live-Check (⚠️ offen, ein Sweep genügt): colour_sweep mit tshirt = 24 (muss Knallrot sein) und trousers = 14 (muss Gelb sein), optional ein dritter Lauf mit 6 (Hellblau). Stimmen die Farben, gilt die Tabelle als verifiziert → Haken hier und in §13 (f) setzen. Stand S11: 24 = Knallrot und 0 = Schwarz sind am Bildschirm bestätigt (parkweiter Rot/Schwarz-Split, Screenshot). Offen ist nur noch ein Lauf mit 14 = Gelb (oder 6 = Hellblau) als Gegenprobe für die Tabellen-Mitte.


5. Heading-Mechanismus (Intent-Lebenszyklus)

5.1 Native Setter (Konkurrenz des LLM)

Alle drei schreiben identisch: +0xC5 = ride, +0xC6 = 200, Pathfind-Goal-Reset, +0xF4 = 0, Fenster-Invalidate 0xc97 (§9). ✅

Setter Adresse Trigger / Eigenheit
mcp_peep_pick_ride_to_go_on 0x43345a ≥ 5×2048 Ticks ohne Ziel ODER Zufall (0x888/0x2000 mit Karte); 21×21-Tile-Scan bzw. alle Rides; Filter rides_been_on + should_go_on_ride; Wahl = max Excitement. Danach ohne Ziel → inlined leave_park @0x42e3d8 (C5=0xff, C6=254, flags|=1, Gedanke 9)
mcp_peep_head_for_nearest_ride_with_flags 0x4331e2 (Setter @0x43342f) Needs (Hunger/Durst/WC ← Gedanken 0x14/15/16) → Ride-Typ-Flags 0x800000/0x1000000/0x2000000; Guard state == 5 (Walking) oder 8 (Sitting); Wahl = nächstgelegener Stand
„Ride again“ in mcp_peep_on_enter_or_exit_ride 0x432d83 (Setter @0x433133) beim Aussteigen (arg2&1 = Exit); setzt ggf. auch favourite_ride

5.2 Konsument: Wie aus dem Intent Bewegung wird

Wer liest +0xC5 eigentlich? Die Kette, pro Game-Tick: ✅

  1. State-Dispatch. update_peep (0x42e630) läuft für jeden Sprite. Was ein Gast tut, bestimmt sein state (+0x2B) über die Jump-Table 0x78ef3c. State 5 = Walking.
  2. Weiterlaufen. Der Walking-Handler ruft mcp_peep_perform_next_action (0x4318db): schiebt den Gast ein Stück den Weg entlang. In den meisten Ticks passiert nur das — der Intent wird noch gar nicht angeschaut.
  3. Tile-Grenze = Entscheidungspunkt. Erst am Rand der aktuellen Weg-Kachel wird die Pathfinding-Funktion gerufen (indirekt über g_pathfinding_fn_by_peep_type 0x78ee8c[peep_type]; für Gäste ist das mcp_guest_path_finding 0x4327eb).
  4. Intent → Zielkoordinate. guest_path_finding liest +0xC5. Ist ein Ziel gesetzt (≠ 0xff) und das Ride offen (ride->status (+0x21) == 1), wird die Ride-ID in einen konkreten Punkt auf der Karte übersetzt. Ein Ride ist nämlich kein Punkt — eine Achterbahn belegt viele Kacheln. Das laufbare Ziel ist der Stationseingang: das Eingangshäuschen eines der 1–4 Bahnsteige, das am Wegenetz hängt (bei Shops/Ständen: die Stand-Kachel selbst). Gewählt wird die nächstgelegene Station (Eingangs-Koordinaten ride+0x42, ein Word pro Station; Fallback Bahnsteig-Anfang +0x2A, Höhe +0x32; bei zwei Stationen entscheidet die Parität von peep->no_of_rides); ihre Position landet in den Goal-Globals 0x8ff19c/9e/a0 (+ Param 0x8ff1a8 = 0xf0). Sonderfall „Gast will gehen“ (flags Bit0): Ziel = Parkeingang 0xa9c59e/a0/a2, Param 0. Warum Globals und nicht Peep-Felder? Sawyer-Idiom: Globals als Funktionsargumente. Sie werden im Update eines Peeps geschrieben und sofort konsumiert (single-threaded, Peeps strikt nacheinander); der nächste Peep überschreibt sie einfach. Dasselbe Muster: g_common_format_args (Argumente) und g_pathing_result (Rückgabewert). Persistent pro Gast ist nur der Goal-Cache +0xCC–CFBridge: wohin Gast X unterwegs ist, steht in dessen +0xCC, nie in den Globals.
  5. Richtungswahl pro Kreuzung. mcp_peep_pathfind_choose_direction (0x436cbd) wählt an der Kachel den Ausgang, der dem Goal am nächsten kommt — an jeder Kreuzung neu, es gibt keinen Routen-Cache. Einziger gespeicherter Zustand ist der kleine Goal-Cache am Peep (+0xCC–CF, plus History +0xD0) — darum wird er im M1-Rezept (§11) zurückgesetzt.

Warum das für die Bridge reicht: Das LLM schreibt genau ein Byte (den Intent, +0xC5). Spätestens an der nächsten Tile-Grenze übersetzt das Spiel ihn selbst in eine Zielkoordinate und steuert Kreuzung für Kreuzung dorthin. Es gibt keine Route, die man berechnen oder invalidieren müsste.

5.3 Wie schlau ist das Routing? (✅ S6, choose_direction komplett gelesen)

Kein A*, keine globale Wegsuche. Pro Kreuzung passiert Folgendes:

  1. Kandidaten sammeln: die begehbaren Ausgänge des Weg-Elements — abzüglich der Richtungen, die laut History an dieser Kreuzung schon probiert wurden.
  2. Ein Kandidat → nehmen. Sonst läuft pro Kandidat eine tiefenbegrenzte Probe (mcp_peep_pathfind_heuristic_search 0x437033): sie folgt dem Wegenetz vorwärts und misst, wie nah sie dem Goal kommt. Die Richtung mit dem besten Wert gewinnt (argmin, Ergebnis-Übergabe wieder per Scratch-Global 0x8ff1a1).
  3. Tiefenbudget (g_pathfind_max_junctions 0x8ff1a4, gezählt in Kreuzungen): 8 für normale Gäste. Beim Parkverlassen: 12 ohne Karte, 14 mit Karte (Item-Bit 2 — die Parkkarte hat echten mechanischen Nutzen, aber nur für die Exit-Suche!), 16 wenn der Countdown unter 90 fällt (verzweifelte Suche).
  4. Anti-Schleifen-Gedächtnis: pathfind_history[4] (+0xD0, Sublayout gelöst: {x, y, z, tried_dir_mask}) merkt sich die letzten 4 Kreuzungen und welche Ausgänge dort schon genommen wurden. Beim Wiederbesuch sind die ausgeblendet; ein neues Goal setzt die History zurück.

Konsequenzen: Um Ecken laufen kann er problemlos — die Probe folgt dem Wegegraph, nicht der Luftlinie. Aber: Umwege, die mehr als ~8 Kreuzungen Voraussicht bräuchten, sieht er nicht; Sackgassen fressen Countdown; breite Doppelwege erzeugen Kreuzungs-Inflation (das berüchtigte Wide-Path-Problem — die Merge-Kaskade dafür steht in choose_direction). Scheitern ist einkalkuliert: Genau dafür existiert das Lost-Trio (§6). Für die Bridge heißt das: Ob ein Intent ankommt, hängt vom Park-Layout und der Distanz ab — der Countdown mit den Schwellen 60/30 ist das beobachtbare Feedback darüber.

5.4 Clear-Stellen

  • Ankunft am Ziel-Shop/Ride @0x431bf4 (state→17 Buying, current_ride +0x68 gesetzt) ✅
  • Timeout Countdown→0 (§6) ✅
  • ride_demolish 0x447990 ✅
  • leave_park: C5=0xff + flags Bit0 ✅
  • Entscheidung am Ziel in mcp_peep_should_go_on_ride 0x4336b2 ✅ (S7, komplett gelesen — ≙ OpenRCT2 ShouldGoOnRide + ShouldGoToShop in einer Funktion): Die Funktion konsumiert den Intent in beiden Ausgängen, jeweils nur wenn das geprüfte Ride == aktuelles Heading ist. 0x4339c7 = Accept-Epilog Ride (Popularity+1, QUEUE_FULL-Bit löschen), 0x433a6d = Accept-Epilog Shop/WC, 0x433b8c = Reject-Epilog (≙ ChoseNotToGoOnRide: previous_ride := Ride + Timeout 0, sofern nicht nur „thinking“). Der Ablehnungsgrund steht danach als frischester Gedanke in Slot 0 (§8) — stille Rejects (sehr übel + heftiges Ride; unbewertetes Ride per 90-%-Würfel oder G-Limits) hinterlassen keinen. Args: arg3 Bit0 = atQueue, Bit1 = Queue-Checks überspringen, Bit2 (Wert 4) = „thinking“ = Fern-Bewertung (unterdrückt Gedanken, previous_ride und Popularity — so ruft pick_ride sie beim Scan)

5.5 Overwrite-Regeln (wer überschreibt den LLM-Intent?)

Quelle Überschreibt LLM-Heading?
pick_ride nie — feuert nur bei heading == 0xff ✅
Needs-Setter ja, außer aktuelles Ziel ist selbst Essen/Trinken/WC (Typ-Flags & 0x3800000) ✅
Ride-again nur beim Aussteigen (heading dort schon 0xff) ✅
leave_park ja (0xff + Flag Bit0 — blockt danach beide Suchen) ✅
Timeout (Lost-Trio §6) ja — Countdown +0xC6 erreicht 0 → check_cant_find_ride schreibt 0xff ✅

Bridge-Folge: Standing Intents brauchen einen Watcher, der Überschreiben erkennt (C5-Wert ≠ gesetzter Wert) und ggf. re-asserted — oder man akzeptiert den Need als legitime Unterbrechung (Spielphilosophie!).


6. Intent-Timeout: das Lost-Trio

Immer als Trio gerufen, 2 Call-Sites: update_peep Walking @0x42ed95/9a/9f (nach perform_next_action, gated g_pathing_result(0x9030d8) & 1 = Tile erreicht) und guest_path_finding @0x4329b9/be/c3. Kadenz ≈ 1 Dekrement pro gelaufener Kachel → Setter-Wert 200 = max. ~200 Kacheln Suche. ✅

Funktion Adresse Verhalten
mcp_peep_check_cant_find_ride 0x438250 heading≠0xff: bei Countdown==60/30 Gedanke 0x17 „Can't find X“ (Arg=Ride) + happiness_target−=30; Countdown−−; bei 0 → heading=0xff + Invalidate 0xc97 = das Intent-Timeout
mcp_peep_check_cant_find_exit 0x4382a1 Guard flags&1 (LEAVING_PARK); Gedanke 0x1b bei 60/30/1; bei 0 → Reset auf 90 (endlos) + News-Item Typ 2, String 0x8d0
mcp_peep_check_if_lost 0x438311 flags&0x400 → Gedanke 0x10 jedes Mal; sonst ab 2 Rides im Park: time_lost(+0xF4)++, bei 254→220 + Gedanke 0x10

Watcher-Events der Bridge: Übergang +0xC5 → 0xff = Timeout/Zielverlust; Countdown 60/30 = Frühwarnung.


7. Stat-Paar-Mechanik (warum Stats RO sind)

+0x38–0x3D sind Ist/Target-Paare: Events schreiben das Target, das 128-Tick-Update (mcp_tick128_update_guest 0x42df82, Gate (sprite_index & 0x7f) == (ticks & 0x7f), ≈ 3,2 s Kadenz) driftet den Ist-Wert graduell nach. ✅ → Target − Ist = Trend (Telemetrie liest beide). Ist-Writes würden zurückdriften — zweiter Grund für RO neben der Maske. Happiness-Dynamik läuft über Minuten → 15–60 s Watcher-Intervall reicht.


8. Gedanken (Diagnose-Kanal)

struct PeepThought { u8 type; u8 pad_01 (Item? ⚠️); u8 freshness; u8 fresh_timeout; } — 5 Slots ab +0xB0, Slot 0 = neuester. Insert: mcp_peep_insert_new_thought 0x436658 (type@al, arg@ah).

Bekannte Typen (S7 stark erweitert — alle aus should_go_on_ride, Nummern == OpenRCT2-Enum): 0x00 Can't afford ride (Arg=Ride) · 0x01 Spent all money (Arg=0xff) · 0x04 More thrilling than X · 0x05 Too intense · 0x07 Sickening · 0x08 Bad value · 0x09 (leave_park-Kontext) · 0x0A Good value · 0x10 Lost · 0x14 Hungry · 0x15 Thirsty · 0x16 WC · 0x17 Can't find ride (Arg=Ride) · 0x18 Not paying that much (WC-Preis) · 0x19 Not while raining · 0x1b Can't find exit · 0x1E Not safe (nach Crash). Vollständige Tabelle: offen (per String-Tabelle im Binary abzuleiten, s. §13).

Ablehnungs-Übersetzung für die Bridge: Nach einem Reject in should_go_on_ride (heading → 0xff, §5.4) steht der Grund als Gedanke in Slot 0 — das Watcher-Event „Intent weg“ plus frischester Gedanke ergibt die Erklärung fürs LLM („zu teuer“, „zu intensiv“, „Regen“, …).


9. Fenster & UI-Invalidierung

  • Setter/Timeout invalidieren mit Muster 0xc97 = Klasse 0x17 (WC_PEEP) + Bit7 (nur ein Widget), Widget 12, Nummer = sprite_index → gezielt eine Zeile des Gästefensters = Statuszeilen-KandidatTODO(#5) (heiße Spur: sub_43576a/sub_435747 lesen +0xC5 in Fenster-Region). ✅
  • mcp_window_invalidate_by_class_number 0x57d173 (al = class+Flags: Bit7 = nur Widget ah, Bit6 = ganze Klasse; bx = number) · mcp_window_invalidate 0x57c28a · mcp_gfx_set_dirty_blocks 0x578b61 ⚠️
  • Fensterliste g_window_list 0xc374b4 (Stride 0x188: class @+0x186, number @+0x40, widgets @+0x2C, x/y @+0x30/32), Next-Slot 0xc3858c.
  • Bridge-Poke-Regel (✅ live S9): Farb-Writes auf +0x30/31 rendern sofort — ohne +0x45, ohne Invalidate (die Weltansicht zeichnet Sprites offenbar ohnehin laufend neu). Invalidate bleibt für Fenster-Inhalte relevant (Gästefenster/Statuszeile), nicht für die Weltansicht.

10. Rides & Globals (RO-Kontext)

Ride-Struct (Basis 0xaa75fc, Stride 0x260, 255 Slots)

Decompiler-Lesehilfe (Sawyer-Idiom #2, gefaltete Konstanten): Ride-Zugriffe erscheinen als *(idx*0x260 + 0xaa7707) — die „krumme“ Konstante ist Array-Basis + Feldoffset in eins gefaltet. Feldoffset = Konstante − 0xaa75fc (Beispiel: 0xaa7707 → +0x10B). Der Decompiler kennt das Array nicht, also immer selbst zurückrechnen. Abhilfe (S6, erledigt): struct Ride ist in BN deklariert und 0xaa75fc als Ride [0xff] = g_rides typisiert. BN entfaltet das Idiom damit sogar vollständig: g_rides[zx.d(...)].num_riders (verifiziert an decrement_num_riders). Zwei kosmetische Artefakte der Transformation: (a) totes Lese-Statement des wegoptimierten Index-Temps (z. B. nacktes arg1->current_ride), (b) der Index erscheint in Offset-Notation arg1->:0x68.b statt als Feldname — semantisch identisch. „Reanalyze Current Function“ räumt das manchmal auf.

Offset Feld Beleg
+0x00 type (u8) ✅ (S7) Index in g_ride_type_flags_table 0x788e20 (s. u.)
+0x02 lifecycle_flags (u16) ✅ (S7) Bit 0x80 = BROKEN_DOWN (should_go filtert), Bit 0x200 = QUEUE_FULL (set @0x433b65, clear im Accept @0x4339e8) — Bitwerte == OpenRCT2 RIDE_LIFECYCLE_*
+0x21 status (1 = open) ✅ guest_path_finding prüft
+0x22 / +0x24 name string id / name args ✅ dekodiert wie Gastnamen (§4.2; Bank 4, gleiche Pool-Slots); Standard-Rides: id < 0x8000 + Nummer in name_args
+0x2A station_starts
+0x32 station_heights
+0x42 station_entrances (word/Station, 0xffff = keine)
+0x4A station_exits (word/Station) ⚠️ Layout-Schluss (RCT1-Struct; Nachbarfelder bewiesen)
+0x52 last_peep_in_queue (word/Station, 0xffff = leer) ✅ (S7) Queue-Voll-Erkennung: Abstand des Neuankömmlings zum letzten Queue-Peep
+0x5A num_peeps_in_queue (u8/Station) ✅ (S7) 0xff = Queue voll (RCT1-Äquivalent des 1000er-Checks)
+0x5E station_train_head_sprites (word/Station) ⚠️ Sprite-Index des ersten Wagens je Station (Quelle der Vehicle-Ketten in update_peep)
+0xAC / +0xAE / +0xB0 max_positive_vertical_g / min_negative_vertical_g / max_lateral_g (s16, 100 = 1 g) ✅ (S7) Limits 5,00 / −4,00 / 4,00 g für unbewertete Rides
+0xC4 sheltered_eighths (u8, Top-3-Bits = Anzahl) ✅ (S7) >>5 ≥ 3 = regentauglich
+0xE8 price (money16, 10-Cent-Einheiten) ✅✅ (S7, TODO(#4) GELÖST) vs. cash_in_pocket → Gedanken 0/1; WC: price·40 > toilet → Gedanke 0x18
+0xF0 excitement u16 (0xffff = undef = „hat keine Wertung“)
+0xF2 intensity (0x3e8 = 10.00 = Obergrenze bei gesetztem Heading)
+0xF4 nausea (u16 Rating) ✅ (S7) ≥ 1,40 && peep-nausea > 160 → stiller Reject
+0xF6 value (money16, 0xffff = undef) ✅ (S7) Zahlungsbereitschaft: price > 2·value → Bad value; ≤ value/2 → Good value; HAS_PAID_FOR_PARK_ENTRY viertelt value
+0xFD window_invalidate_flags (u8) ✅ S6: Bits 2|3 = Haupt-Tab | Ride-Liste neu zeichnen
+0x10B num_riders (u8) ✅ S6: Fahrer-Zähler; decrement_num_riders zählt runter, wenn ein Peep OnRide/EnteringRide verlässt
+0x15E last_crash_type (u8, 0 = keiner) ✅ (S7) ≠0 && happiness < 225 → Gedanke 0x1E Not safe

Ride-Typ-Tabellen (§13 c GELÖST, S7): g_ride_type_flags_table 0x788e20, Stride 8, u32-Flags bei +0, Index = ride->type. Bitwerte identisch mit OpenRCT2 RIDE_TYPE_FLAG_*: 0x20000 IS_SHOP (verzweigt should_go_on_ride in den Shop-Pfad), 0x800000 SELLS_FOOD / 0x1000000 SELLS_DRINKS / 0x2000000 IS_TOILET (die 0x3800000-Maske der Needs-Suche; head_for_nearest liest exakt diese Tabelle, Wunsch-Maske via Scratch-Global g_ride_search_type_flags_mask 0x8ff088). Zweite Tabelle g_ride_type_props2_table 0x789370 (Stride 8): +0 u8 ≠0xff gated die WC-Preislogik (vermutlich „bedientes Bedürfnis“ ⚠️), +4 u32-Flags mit 0x10 = PEEP_CHECK_GFORCES.

Globals

Adresse Name Semantik
0xaa75f8 ticks Game-Ticks (Tick128-Gate) ✅
0x903d60 scenario_ticks
0x9030d8 g_pathing_result Bit0 = Tile erreicht (gated Lost-Trio) ✅
0x8ff19c/9e/a0 g_pathfind_goal_x/y/z +Param 0x8ff1a8 (0xf0 = Ride, 0 = Parkeingang) ✅
0xa9c59e/a0/a2 g_park_entrance_x/y/z
0xaf8086–0xaf8093 Climate-Block (g_climate_*) ✅ (S12) komplett kartiert → Unterabschnitt „Wetter-Statemachine“ unten. In der Bridge als benannte Globals, RW bis auf climate_type. Highlights: 0xaf808c temperature (S6: ≥ 0x15 → Gäste schneller durstig, tick128 @0x42e382); 0xaf8092 rain_level ≠ 0 = es regnet (S7: gated NOT_WHILE_RAINING-Gedanken @0x433854)
0xa9c598 g_park_flags ✅ (S7): Bit 0x800 = PARK_FREE_ENTRY (unterdrückt Good-value-Gedanken); Bit 0x100 am Entry von should_go geprüft (Kandidat NO_MONEY ⚠️)
0x8ff088 g_ride_search_type_flags_mask ✅ (S7) Scratch-Argument der Needs-Suche (§10 Typ-Tabellen)
0x78f130 g_nausea_tolerance_thresholds ✅ (S7) {u16,u16}×4, Index nausea_tolerance&3
0xa9d97c g_num_rides ⚠️ Hypothese (OpenRCT2-Match im Lost-Trio)
0xc0d462 g_common_format_args News/String-Formatierung ✅
0xa9f5f8 g_custom_string_pool ✅ (S8) 1024 × 32 Bytes, alle Custom-Namen (§4.2)
0xa9c588 / 0xa9c58c g_park_name_string_idx / g_park_name_args ✅ (S8) Park-Name (gleiches ID-Schema)
0x79570c g_real_name_firstname_ptrs ✅ (S8) 1024 Vornamen-Pointer für IDs 0xa000+ (§4.2)
0x8a220c g_string_table_ptrs ✅ (S8) Pointer-Array der Standard-Strings (Index = String-ID)
0xa9f2d9 g_banners ✅ (S8) Stride 8: +0 Flags, +1 text_string_idx u16, +3 Farbe, +4 Textfarbe, +5/+6 x/y
0x793a74 Easter-Egg-Namenstabelle Caesar−1-kodiert, Terminator 0xff; Deluxe um Expansion-Namen erweitert ✅; Index→Flag-Map in §4.2
0x78ef3c / 0x78ee8c / 0x788194 Jump-Tables peep-state / pathfinding-by-type / game-commands ✅

Wetter-Statemachine (✅ S12, Rest von TODO(#7) gelöst)

Der Climate-Block ist ein Current/Next-Paar-Layout; alle Adressen ✅ aus mcp_climate_update 0x4548a9 und mcp_climate_determine_future_weather 0x454814:

Adresse Feld Semantik
0xaf8086 climate_type (u8) Szenario-Klima, Index in die Monats-Tabelle 0x798324 — in der Bridge bewusst RO (Szenario-Konfiguration)
0xaf8088 update_timer (u16) Countdown, 1/Tick. Solange ≠ 0 friert er die Überblendung ein; das Spiel setzt nach jedem Wetterwurf 0x780 (1920 Ticks ≈ 48 s Wetter-Haltezeit). Bei == 0x3c0 wird Toolbar-Dirty-Flag 8 (0x88adfc) gesetzt (Wettersymbol)
0xaf808a / 0xaf808b weather / weather_next (u8) 0 Sonne · 1 leicht bewölkt · 2 bedeckt · 3 Regen · 4 Platzregen · 5 Gewitter
0xaf808c / 0xaf808d temperature / _next (u8) Basistemperatur (Klima+Monat) + Delta aus der Wettertabelle
0xaf808e / 0xaf808f weather_effect / _next (u8) 0 keiner · 1 Regen · 2 Sturm (Blitz/Donner)
0xaf8090 / 0xaf8091 weather_gloom / _next (u8) 0–2 Verdüsterung; jeder Schritt ruft den Screen-Invalidator
0xaf8092 / 0xaf8093 rain_level / _next (u8) 0 kein · 1 leicht · 2 stark — das Regen-Overlay hängt direkt hieran

Update-Zyklus (mcp_climate_update 0x4548a9, jeden Tick): timer ≠ 0 → nur dekrementieren. Sonst alle 128 Ticks (ticks & 0x7f == 0) genau EIN Schritt Richtung Next, in fester Reihenfolge mit Return nach jedem Schritt: Temperatur ±1 → Gloom ±1 (mit Invalidate) → Effekt (Sprung) → Regenlevel ±1 (Sonderfall next == 3: Direktsprung) → wenn alles gleich: weather := weather_next und Neuwurf.

Neuwurf (mcp_climate_determine_future_weather 0x454814): Monats-Verteilung 0x798324[climate][monat & 7]{base_temp, n, dist[n]}; weather_next = dist[rand % n] (Monat aus 0x903d5c, u16-Zähler, low 3 Bits = Monat ⚠️). Parameter dazu liefert mcp_climate_get_weather_params 0x45486a aus der Wettertabelle 0x798620 (Stride 8: temp_delta s8, effect, gloom, rain_level, +4 sprite_id u32) — Werte byte-identisch mit OpenRCT2 ClimateWeatherData: Sonne +10/0/0/0 · leicht bewölkt +5/0/0/0 · bedeckt 0/0/0/0 · Regen −2/1/1/1 · Platzregen −4/1/2/2 · Gewitter +2/2/2/2. Danach timer := 0x780.

Schreibrezept „Regen erzeugen“ (snippets/make_rain.py, live verifiziert S12): Den NEXT-Block aufs Wunschwetter setzen (weather_next, temperature_next ≈ aktuelle Temp − delta_alt + delta_neu, effect_next, gloom_next, rain_level_next) und timer := 0 — dann blendet das Spiel selbst um (ein Schritt pro 128 Ticks, inkl. korrekter Invalidierung) und würfelt nach der Konvergenz wie gewohnt weiter; der Eingriff heilt also von selbst. Für sofortigen Regen zusätzlich weather/effect/rain_level im CURRENT-Block setzen (Temperatur und Gloom sanft nachlaufen lassen). Live-Beweis S12: Regie von „Gewitter im Anflug“ auf „leichter Regen sofort“ — Statemachine senkte Gloom 2→1 gegen den natürlichen Trend, übernahm weather 3 als Current und würfelte Sonne als Folgewetter.


11. M1-Schreibrezept: „Gast idx zu Ride X schicken“

Vorbedingungen (Bridge prüft): sprite_identifier != 0xff, peep_type == 0, state == 5 (Walking), Ride X existiert und status == 1.

base = 0x963d70 + idx*0x100
write u8   base+0xC6 = 200          # Countdown neu aufziehen (~200 Kacheln)
write u8   base+0xF4 = 0            # time_lost zurücksetzen
write u32  base+0xCC = 0xFFFFFFFF   # Pathfind-Goal-Cache invalidieren
write u8   base+0xC5 = X            # Intent scharf schalten (zuletzt!)

Reihenfolge: +0xC5 zuletzt — der Konsument liest das Feld an jeder Tile-Grenze; so ist der Zustand konsistent, wenn der Intent sichtbar wird. Danach optional Invalidate (§9) für die UI. Massen-Ops (800 Gäste): nicht als Einzel-Tool-Calls, sondern als exec-Snippet (Loop im Bridge-Prozess).


12. Watcher & empirische Tests (Phase C)

Watcher-Architektur: register_watch(condition_script, interval, action=notify|autofix) — billige Bedingungen laufen als Bridge-Daemon, bei Trigger wird das LLM geweckt, liest Thoughts (§8) und setzt neuen Intent. „Glücklich halten“ geht by design nicht über Stat-Writes, nur über legitime Intents nach Diagnose.

Standard-Events: +0xC5 → 0xff (Timeout) · Countdown 60/30 (Frühwarnung) · Regen-Global (Schirm-Szenario) · flags Bit0 (Gast will gehen).

Empirische Tests vor M1 (billig, je 1 Poke):

  1. ✅ (S9) +0x30/31 Kleidungsfarben: rendern sofort, kein Invalidate. Farbtabelle steht seit S10 in §4.3 (aus OpenRCT2 rct1/Tables.cpp); offen ist nur noch der Live-Check: Sweep mit 24 = Knallrot, 14 = Gelb.
  2. ✅ (S11) Bit 0x10 Winken: Engine animiert selbst. Nachweis: Action-Byte +0x71 nimmt den Wert 0x18 nur in der geflaggten Gruppe an (15 Test- gegen 15 Kontroll-Gäste, Histogramm-Vergleich). 0x40 (Foto) und 0x80 (Malen) weiterhin einzeln testen.
  3. ✅ (S11, direkt massenhaft) M1-goto funktioniert: 146 Gäste zum Riesenrad, danach 153 bzw. 230 zum Hecken-Irrgarten geschickt — Gäste laufen los, die Ziel-Queue füllt sich, ab voller Queue lehnen Ankömmlinge ab (should_go_on_ride), Kreuzungsverhalten wie in §5.3 beschrieben (8-Kreuzungen-Budget). leave_park (Bit0) ebenfalls massenhaft bestätigt: von gut 300 geflaggten Gästen hatten kurz darauf ~180 den Park real verlassen.

13. Offene Punkte (Verifikations-Backlog)

# Punkt Methode / Einstieg
TODO(#2) GELÖST (S6): +0x3E hunger, +0x3F thirst, +0x40 toilet, +0x42 time_to_consume (RCT2-Layout) Beweise: tick128 0x42e29d/0x42e382/0x42e513 (Gedanken 0x14/0x15, Hitze→Durst, Konsum-Block). Die „Queue-Ketten“ über +0x3E waren Vehicles (Züge aus Station-Array 0xaa765a) — Peep-Queue-Verkettung in RCT1 weiterhin unkartiert (falls existent).
TODO(#3) GELÖST (S8): Pool 0xa9f5f8 (1024×32), IDs 0x8000–0x8FFF, Slot = id&0x3ff; Lese-/Schreibrezept + Easter-Egg-Map in §4.2; Peep-Liste ist namenssortiert (§3) set_guest_name/register/free/format komplett gelesen; „echte Namen“ (IDs 0xa000+) als Beifang dekodiert.
TODO(#4) GELÖST (S7): price = ride+0xE8 (money16); Ablehnungsgründe = Gedanken (§8); die drei 0xff-Stellen sind 2× Accept (0x4339c7 Ride, 0x433a6d Shop) + 1× Reject (0x433b8c) — Details §5.4/§10 should_go_on_ride komplett gelesen, ≙ OpenRCT2 ShouldGoOnRide+ShouldGoToShop.
TODO(#5) Statuszeile Gästefenster (Widget 12) sub_43576a decompilen.
TODO(#6) sub_4379ec = update_sprite_type? — Trigger-Verhalten GELÖST (S13, empirisch): Item-Bit 4 + Regen lässt die Engine sprite_type 18 (Schirm) setzen, außer auf dem Ride; Beleg in §4 bei +0x2D. Offen bleibt nur der Decompile von sub_4379ec selbst (Bestätigung als update_sprite_type, weitere Typen-Prioritäten) Test: Maske um +0xF7/+0xFC(Byte 0) erweitert, 271 Gäste mit Schirm + make_rain → 219× Typ 18. Decompile + Xref auf Regen-Global weiter offen.
TODO(#7) GELÖST (S7 + S12): 0xaf8092 = g_climate_rain_level, ≠0 = regnet (Beweis: NOT_WHILE_RAINING-Gate @0x433854). S12: Wetter-Statemachine komplett kartiert (§10 „Wetter-Statemachine“), Climate-Block per write_global schreibbar, Regen live erzeugt (make_rain.py). Restfrage: 0x903d5c als Monatszähler ist Schluss aus & 7 ⚠️ S7 gratis aus should_go_on_ride; S12 via Xrefs auf 0xaf8092 → mcp_climate_update.
TODO(#8) Scan-Reste: sub_437f50 (3× heading-Read), sub_40f50d (C5=cl/C6=0xf0 → Staff-Hire?), sub_436754 (W 0xff @0x4369e0); Staff-Renames sub_44f645/44f89b Triage, niedrige Prio.
(a) Gedanken-Typen-Tabelle vervollständigen String-Tabelle im Binary.
(b) pick_ride prüft Item-Bit1 (&2) statt Map-Bit2 (&4) RCT1-Bug oder Fehldeutung? Asm-Stelle nachlesen.
(c) GELÖST (S7): g_ride_type_flags_table 0x788e20 (Stride 8, u32 @+0); head_for_nearest liest sie mit der 0x3800000-Maske, Bitwerte == OpenRCT2 (0x20000 IS_SHOP verifiziert). Zweittabelle g_ride_type_props2_table 0x789370 (+0 u8 ⚠️, +4 Flags: 0x10 CHECK_GFORCES) Details §10.
(d) pad_e0 (+0xE2?), field_6d/6e/70 (pathfind_history ✅ S6) beim nächsten Struct-Pass.
(e) peep_flags Bit 2 (0x4): wird in choose_direction mit ~11 % zufällig gelöscht — Bedeutung? Xrefs auf Bit-Setzer suchen.
(f) RCT1-Farb-Enum (+0x30/31, betrifft auch Ride-/Ballon-Farben): Tabelle steht seit S10 in §4.3 (Weg 1: OpenRCT2 rct1/Tables.cpp, RCT1::GetColour; erklärt die S9-Beobachtung 18 = lightBrown) S11: 24 = Knallrot und 0 = Schwarz live bestätigt (Rot/Schwarz-Split + Screenshot). Rest: ein Lauf mit 14 = Gelb, dann Haken setzen. Fallback-Wege 2 (Screenshot-Läufe) und 3 (BN-Paint-Scan) nur nötig, falls der Check scheitert.

14. Funktions-Verzeichnis (alle in BN benannt/kommentiert)

Adresse Name Rolle
0x42cdec (sub) Master-Startup/Update rct: sub_4385d8
0x42dbfd mcp_update_all_peeps Sprite-Loop
0x42e630 mcp_update_peep Prototyp void(struct Sprite* peep @ esi) — Trick für alle esi-Konvention-Funktionen
0x42df82 mcp_tick128_update_guest 128-Tick-Update (§7)
0x42df13 mcp_peep_check_for_path alle 16 Ticks (+0xC4)
0x4318db mcp_peep_perform_next_action Walking-Kern
0x431628 mcp_peep_footpath_move_forward
0x432516 mcp_peep_get_height_on_slope
0x4327eb mcp_guest_path_finding Heading-Konsument (§5.2)
0x436cbd mcp_peep_pathfind_choose_direction pro Kreuzung (§5.3)
0x437033 mcp_peep_pathfind_heuristic_search tiefenbegrenzte Probe pro Kandidat (§5.3)
0x437028 mcp_peep_reset_pathfind_goal dword +0xCC = 0xffffffff
0x43345a mcp_peep_pick_ride_to_go_on Setter #1
0x4331e2 mcp_peep_head_for_nearest_ride_with_flags Setter #3 (Needs)
0x432d83 mcp_peep_on_enter_or_exit_ride Setter #2 „ride again“ @0x433133
0x4336b2 mcp_peep_should_go_on_ride Entscheidung am Ziel (§5.4) — ≙ ShouldGoOnRide+ShouldGoToShop, Preis/Value/Intensität/Queue ✅ S7
0x436ac6 mcp_ride_update_popularity al = 1 dafür / 0 dagegen, edi = ride_id·0x260 ✅ S7
0x43729b mcp_peep_decide_and_buy_item
0x436658 mcp_peep_insert_new_thought type@al, arg@ah
0x438250 / 0x4382a1 / 0x438311 Lost-Trio §6
0x436af8 / 0x436b1e decrement_num_riders / window_state_update
0x43ab04 mcp_allocate_peep (= create_sprite)
0x43a9a9 mcp_reset_sprite_lists Free-List-Init
0x43ac4f mcp_sprite_remove Despawn: Name freigeben, Slot 0xff, Spatial-Unlink ✅ S8
0x43588a mcp_game_command_set_guest_name 3-Chunk-Kommando + Easter-Egg-Reeval (§4.2) ✅ S8
0x448438 mcp_game_command_set_ride_name gleiches Muster, Ride-ID in bh ✅ S8
0x41169f mcp_game_command_set_park_name Park-Name-Globals 0xa9c588/8c ✅ S8
0x45444d mcp_register_custom_string Pool-Alloc; Selektor: Bit7 = Dups ok, Bits 0–6 = Bank ✅ S8
0x4544dc mcp_free_custom_string Slot-Byte 0 = 0 für IDs 0x8000–0x8FFF ✅ S8
0x45272e mcp_format_string_id_to_buffer Name-Resolver, 4 ID-Räume (§4.2) ✅ S8
0x435b05 mcp_reinsert_peep_in_name_sorted_list hält Peep-Liste namenssortiert (§3) ✅ S8
0x44b3e5 mcp_window_banner_event_handler Banner-Array g_banners 0xa9f2d9 ✅ S8
0x436c6d Easter-Egg-Matcher Caesar−1-Namen
0x433c4a Guest-Window-Dispatcher TODO(#5)
0x434e47 mcp_guest_window_draw_inventory Voucher-Beweis
0x4471fa / 0x447990 mcp_ride_create / mcp_ride_demolish
0x41188c mcp_calculate_park_rating Lost-Zählung (+0xC6 ≤ 0x59 bei flags&1)
0x4548a9 mcp_climate_update Wetter-Statemachine, 1 Schritt/128 Ticks (§10) ✅ S12
0x454814 mcp_climate_determine_future_weather Neuwurf aus Monats-Verteilung 0x798324, timer := 0x780 ✅ S12
0x45486a mcp_climate_get_weather_params Wettertabelle 0x798620 → temp/effect/gloom/rain ✅ S12
0x4547e5 mcp_climate_reset Szenario-Start: setzt climate_type + Erstwetter ✅ S12
0x415ecf mcp_news_item_add
0x576951 mcp_scenario_rand
0x57d173 / 0x57c28a / 0x578b61 Window-Invalidate-Familie §9
0x40f7fa game_do_command Jump-Table 0x788194

Anhang A: HLIL-Notations-Spickzettel (Binary Ninja)

Notation Bedeutung
zx.d(x) / sx.d(x) zero-/sign-extend auf 32 Bit (= movzx/movsx)
x.b / x.w / x.d Byte-/Word-/Dword-Teil von x (Register: eax.b = al) bzw. Zugriffsbreite
x:1.b Byte Nr. 1 von x (eax:1.b = ah)
arg1->:0x68.b Member-Zugriff per Roh-Offset statt Feldname (hier ≡ current_ride) — Renderer-Artefakt nach Umbau-Passes, semantisch identisch
nacktes arg1->feld als eigene Zeile toter Rest eines wegoptimierten Temps (Load ohne Verbraucher) — ignorieren
u< / u>= … vs. s< / s>= vorzeichenlose vs. vorzeichenbehaftete Vergleiche
rol.w / ror.w Rotationen — Sawyers x/y-Hash in die Tile-Pointer-Map 0xc0d4b4
*(x + 0xKRUMM) gefaltete Array-Basis + Feldoffset (§10 Lesehilfe)
@ esi im Prototyp Argument wird im Register übergeben (Sawyer-Konvention; Prototyp setzen macht Zugriffe zu peep->feld)

Pflegehinweis: Dieses Handbuch ist destillierte Semantik — die Chronik (inkl. widerlegter Hypothesen) bleibt in den Memories rct-deluxe-peep-mapping / rct-mcp-peep-control-goal. Neue Erkenntnisse: erst BN-Struct/Kommentare aktualisieren, dann hier den TODO-Marker auflösen.


Lizenz: CC BY 4.0 © 2026 Lutz Leonhardt