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) · TODO(#N) = offener Punkt, Nummer aus handoff.md
Drei Schichten, nur die mittlere wird ersetzt:
- Bedürfnis-Simulation (Happiness/Energy/Nausea/Hunger…) = unangetastet. Stats sind Reward-Funktion, keine Schreibziele.
- Entscheidung = LLM setzt Intentionen: Ziel-Ride über den Heading-Mechanismus (§5). Stände/WCs sind intern Rides.
- 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.
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).
- 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 Heads0xa9c570/ Counts0xa9c57c. Offset 4 = Peep-Liste (Headg_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 0x435b05hängt einen Peep nach jedem Namenswechsel aus und fügt ihn per String-Vergleich an der richtigen Stelle wieder ein. Ein Bridge-Write auf+0x22lässt die Verkettung intakt, nur die Sortierung wird stale (kosmetisch, betrifft das Gästelisten-Fenster). - Stale-Index-Gefahr:
sprite_indexhat keinen Generation-Counter — Bridge muss vor jedem Zugriff prüfen, dass der Slot noch ein Gast ist (sprite_identifier != 0xffundpeep_type == 0). - Spatial-Index
g_sprite_spatial_index 0xc2d4f6(128×128-Buckets, Off-Map-Bucket0xc354f6), verkettet übernext_in_quadrant (+0x02). Nur RO-Hintergrundwissen.
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 |
| 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 |
| 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 |
| 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 |
| 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) |
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).
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):
id= u16 anpeep+0x22lesen.- Wenn
0x8000 ≤ id < 0x9000: Custom-Name, Text = C-String an0xa9f5f8 + (id & 0x3ff)·0x20. Fertig. - Wenn
0xa000 ≤ id < 0xe000: „echter Gästename“ (Option Show real guest names). Vorname = String hinter Pointerg_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.“. - Sonst (
id < 0x8000): Standard-String wie „Guest {Nr}“ aus der Stringtabelle (Pointer-Arrayg_string_table_ptrs 0x8a220c[id]); die Gastnummer steckt inname_args +0x9C.
Beweis: mcp_format_string_id_to_buffer 0x45272e implementiert exakt diese vier Zweige.
Schreib-Rezept M2 „Gast idx heißt jetzt Claude“:
- Freien Slot suchen:
smit Byte 0 == 0 an0xa9f5f8 + s·0x20(linear scannen, wie das Original). - Text (≤ 31 Zeichen + NUL) in den Slot schreiben.
- Alte ID an
peep+0x22merken. - u16
peep+0x22 = 0x8800 + sschreiben. - War die alte ID selbst custom (
0x8000 ≤ alt < 0x9000): alten Slot freigeben (Byte 0 = 0) — sonst leakt der Pool. - 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 (
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 (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.
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 |
Wer liest +0xC5 eigentlich? Die Kette, pro Game-Tick: ✅
- State-Dispatch.
update_peep (0x42e630)läuft für jeden Sprite. Was ein Gast tut, bestimmt seinstate (+0x2B)über die Jump-Table0x78ef3c. State 5 = Walking. - 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. - 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 dasmcp_guest_path_finding 0x4327eb). - Intent → Zielkoordinate.
guest_path_findingliest+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-Koordinatenride+0x42, ein Word pro Station; Fallback Bahnsteig-Anfang+0x2A, Höhe+0x32; bei zwei Stationen entscheidet die Parität vonpeep->no_of_rides); ihre Position landet in den Goal-Globals0x8ff19c/9e/a0(+ Param0x8ff1a8 = 0xf0). Sonderfall „Gast will gehen“ (flags Bit0): Ziel = Parkeingang0xa9c59e/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) undg_pathing_result(Rückgabewert). Persistent pro Gast ist nur der Goal-Cache+0xCC–CF— Bridge: wohin Gast X unterwegs ist, steht in dessen+0xCC, nie in den Globals. - 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.
Kein A*, keine globale Wegsuche. Pro Kreuzung passiert Folgendes:
- Kandidaten sammeln: die begehbaren Ausgänge des Weg-Elements — abzüglich der Richtungen, die laut History an dieser Kreuzung schon probiert wurden.
- 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-Global0x8ff1a1). - 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). - 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.
- Ankunft am Ziel-Shop/Ride @0x431bf4 (state→17 Buying,
current_ride +0x68gesetzt) ✅ - 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 — ≙ OpenRCT2ShouldGoOnRide+ShouldGoToShopin 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)
| 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!).
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.
+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.
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“, …).
- 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-Kandidat →TODO(#5)(heiße Spur:sub_43576a/sub_435747lesen +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-Slot0xc3858c. - Bridge-Poke-Regel (✅ live S9): Farb-Writes auf
+0x30/31rendern 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.
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) | |
| +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) | |
| +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“
| 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 ✅ |
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 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.
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).
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):
- ✅ (S9)
+0x30/31Kleidungsfarben: 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. - ✅ (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.
- ✅ (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.
| # | 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. |
| 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 |
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. |
| 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 |
| 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