2026-04-02 23:11:15 -07:00
# RT3 Reverse-Engineering Handbook
This handbook is the project bootstrap for reverse-engineering and rewriting Railroad Tycoon 3.
It is written for future us first: enough structure to resume work quickly, without pretending the
project is already mature.
## Canonical Target
- Canonical executable: `rt3_wineprefix/drive_c/rt3/RT3.exe` (patch 1.06)
- Reference executable: `rt3_wineprefix/drive_c/rt3_105/RT3.exe` (patch 1.05)
- Canonical SHA-256: `01b0d2496cddefd80e7e8678930e00b13eb8607dd4960096f527564f02af36d4`
- Reference SHA-256: `9e96b0695cb722a700f99c8dce498d34da7235e562b1e275bcc1764f8c9b7eb1`
## Documents
- `setup-workstation.md` : toolchain baseline and local environment setup.
- `re-workflow.md` : how to analyze the binary, record findings, and export reusable artifacts.
- `function-map.md` : canonical schema and conventions for function-by-function mapping.
2026-04-11 17:55:16 -07:00
- `control-loop-atlas.md` : compatibility index for the split atlas, preserving legacy anchors.
- `control-loop-atlas/` : canonical section files for the atlas narrative.
2026-04-10 01:22:47 -07:00
- `runtime-rehost-plan.md` : bottom-up runtime replacement plan and milestone breakdown.
2026-04-02 23:11:15 -07:00
## Repo Conventions
- `docs/` : stable project guidance and durable design notes.
- `tools/py/` : committed Python helpers for analysis and validation.
- `artifacts/exports/` : committed derived outputs that can be regenerated.
- Local-only state stays untracked: `.venv/` , Ghidra projects, Rizin databases, crash dumps, and other
bulky/generated working files.
## Current Baseline
The current technical milestone is a repeatable loop-mapping workflow for the 1.06 executable.
Before injection work or deep file-format work, we capture:
- executable hashes and PE metadata
- section layout, imports, and notable strings
- a starter subsystem inventory plus a control-loop atlas
- focused address and string context exports for branch-deepening passes
- a reusable CLI RE kit for branch dossiers where the atlas needs deeper grounding
- a stable curated function ledger in `artifacts/exports/rt3-1.06/function-map.csv`
Current coverage is broad enough to support future sessions without rediscovery, especially in:
- CRT startup and bootstrap handoff
- shell frame, layout, presentation, deferred-message, and frontend overlay flow
- Multiplayer.win UI, chat, session-event, and transport ownership
- map/scenario load and text-export paths
- shared support layers such as intrusive queues, vectors, hashed stores, and tracked heaps
README maintenance rule:
- Keep this section at subsystem level only.
- Do not mirror per-pass function additions here.
- Detailed mapping progress belongs in `artifacts/exports/rt3-1.06/function-map.csv` and the derived branch artifacts under `artifacts/exports/rt3-1.06/` .
Current local tool status:
- Ghidra is installed at `~/software/ghidra`
- `~/software/ghidra/ghidraRun` launches successfully in an interactive shell
- Rizin is installed and available on `PATH`
- `winedbg` works with `rt3_wineprefix`
- RT3 launches under `/opt/wine-stable/bin/wine` when started from `rt3_wineprefix/drive_c/rt3`
## Next Focus
2026-04-14 19:37:53 -07:00
The atlas milestone is broad enough that the next implementation focus has already shifted downward
into runtime rehosting. The current runtime baseline now includes deterministic stepping, periodic
2026-04-14 20:01:43 -07:00
trigger dispatch, normalized runtime effects, staged event-record mutation, fixture execution,
2026-04-14 21:19:08 -07:00
state-diff tooling, tracked save-slice documents for captured-runtime inputs, overlay import
documents that combine captured snapshots with save-derived state, and a packed-event persistence
bridge that now reaches per-record summaries and selective executable import.
2026-04-02 23:11:15 -07:00
2026-04-14 19:37:53 -07:00
The highest-value next passes are now:
- preserve the atlas and function map as the source of subsystem boundaries while continuing to
avoid shell-first implementation bets
2026-04-15 09:13:51 -07:00
- keep using overlay imports as the context bridge when selectively executable packed rows still
2026-04-16 17:08:03 -07:00
need runtime context that current save slices and raw save inspection do not yet persist
2026-04-15 12:11:29 -07:00
- treat broader real grouped-descriptor recovery as the active packed-event frontier now that the
first company-scoped batch already parses, summarizes, and executes through the ordinary runtime
path when overlay context resolves its symbolic company scope: descriptor `2` `Company Cash` ,
descriptor `13` `Deactivate Company` , and descriptor `16` `Company Track Pieces Buildable`
2026-04-15 23:24:08 -07:00
- descriptors `1` `Player Cash` and `14` `Deactivate Player` now join that executable real batch
through the same ordinary runtime path, backed by the minimal player runtime and overlay-import
context
2026-04-16 16:07:10 -07:00
- the first chairman-targeted real grouped rows now execute too through that same path when the
2026-04-16 18:03:17 -07:00
hidden grouped target-subject lane resolves to grounded chairman scope ordinals `0..3` :
`condition_true_chairman` , `selected_chairman` , `human_chairmen` , and `ai_chairmen` ; wider
chairman ordinals stay parity-only under `blocked_chairman_target_scope`
2026-04-16 16:36:48 -07:00
- chairman runtime ownership is broader now too: selected-chairman condition rows for chairman
cash, holdings value, net worth, and purchasing power import through the same service path, and
2026-04-16 17:30:42 -07:00
the first grounded company governance issue batch now executes too via book-value-per-share,
investor-confidence, and management-attitude thresholds; wider chairman target ordinals remain
frontier
2026-04-16 17:08:03 -07:00
- checked-in save-slice documents can now carry explicit company rosters and chairman profile
tables too, so the current company-targeted and chairman-targeted descriptor/condition batches
can execute from standalone save-slice fixtures without overlay snapshots when that context is
2026-04-17 14:56:41 -07:00
present; raw `.gms` inspection/export now reconstructs those company/chairman collections
directly from save-side tagged record families, while the fixed save-side `0x32c8` world block
2026-04-17 17:10:43 -07:00
still provides selected company/chairman ids plus the grounded campaign-override, issue-`0x37`
value/multiplier, and chairman-slot / role-gate analysis bytes; the current raw-save frontier is narrower now:
2026-04-17 16:36:48 -07:00
company/chairman identity, active flags, links, chairman cash, chairman holdings, chairman
purchasing power, company debt, company track-laying capacity, and collection counts are
grounded, while broader company
2026-04-17 14:56:41 -07:00
finance/governance scalar lanes plus controller-kind reconstruction still remain conservative
defaults until their raw offsets are pinned more strongly; the offline analysis command
`runtime inspect-save-company-chairman <save.gms>` now dumps those remaining raw record
2026-04-17 15:18:13 -07:00
candidates directly from the rehosted parser, including fixed-world chairman slot / role-gate
2026-04-17 17:10:43 -07:00
context, the grounded fixed-world issue-`0x37` pair, the separate six-float economic tuning
band, derived holdings-at-share-price and cached purchasing-power totals,
2026-04-17 18:28:53 -07:00
context, company dword candidate windows, and richer chairman qword cache views; the current
rehosted company-side owner state now also includes a typed market/cache map carrying saved
outstanding-shares, support/share-price/cache words, salary lanes, calendar words, and
connection latches for each live company, so later finance/stat-family readers can attach to
owned runtime data instead of one more guessed save offset
2026-04-17 17:10:43 -07:00
- the project rule on the remaining closure work is now explicit too: when one runtime-facing field
is still ambiguous, prefer rehosting the owning source state or real reader/setter family first
instead of guessing another derived leaf field from neighboring raw offsets
2026-04-16 19:03:07 -07:00
- a checked-in `EventEffects` export now exists at
2026-04-16 20:20:41 -07:00
`artifacts/exports/rt3-1.06/event-effects-table.json` , and a checked-in semantic closure layer
now exists at `artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json`
- recovered descriptor rows now land on explicit semantic frontier buckets such as
`blocked_shell_owned_descriptor` , `blocked_evidence_blocked_descriptor` , and
`blocked_variant_or_scope_blocked_descriptor` instead of generic unmapped-descriptor residue
- the first recovered governance descriptor tranche now executes through the generic
company-governance scalar effect surface: descriptor `56` `Credit Rating` and descriptor `57`
`Prime Rate`
2026-04-16 19:03:07 -07:00
- adjacent recovered finance/control-transfer descriptors such as `55` `Stock Prices` and `58`
`Merger Premium` now land on explicit shell-owned descriptor parity instead of generic unmapped
2026-04-17 13:01:26 -07:00
descriptor residue, with tracked fixtures now pinning finance, scenario-outcome, and
control-transfer shell rows explicitly
2026-04-16 20:20:41 -07:00
- the recovered whole-game scalar economy/performance strip `59..104` now has a bounded runtime
landing surface too: representative rows execute into `RuntimeState.world_scalar_overrides`
through stable normalized keys such as `world.build_stations_cost` and
`world.track_maintenance_cost`
2026-04-17 08:18:34 -07:00
- the runtime-variable strip `39..54` now executes too through bounded event-owned scalar maps on
2026-04-17 08:50:35 -07:00
world/company/player/territory state, and the matching ordinary-condition strip now gates
imported records through those same runtime-owned variable maps; these variables are still
runtime-owned only in the current model and are not yet reconstructed from raw saves
2026-04-16 21:42:20 -07:00
- the grounded aggregate cargo-economics descriptors now execute too: descriptor `105`
`All Cargo Prices` and descriptors `177..179` `All Cargo Production` / `All Factory Production`
2026-04-16 23:44:55 -07:00
/ `All Farm/Mine Production` land on bounded event-owned cargo override state, and the grounded
named cargo-production strip `180..229` now lands on named cargo production overrides too
2026-04-17 13:41:44 -07:00
- the named cargo-price strip `106..176` now lands on named cargo price overrides too; the
checked-in static selector reconstruction is now explicit because the broader 1.06 CargoTypes
corpus has `51` names, the Cargo106 `cargoSkin` corpus has `70` , and the rehosted offline
selector builder now closes the 71-row price strip as `cargoSkin` plus the core `Rock`
carry-over; a new
2026-04-17 11:49:20 -07:00
checked-in offline cargo-source report at
`artifacts/exports/rt3-1.06/economy-cargo-sources.json` now parses both `CargoTypes` and the
`Cargo106.PK4` `cargoSkin` descriptors through rehosted code, normalizes localized
2026-04-17 13:41:44 -07:00
`~####Name` tokens into visible names, builds a merged live cargo registry, and now derives
exact named cargo-production and named cargo-price selectors from the checked-in bindings;
dedicated CLI inspector commands now expose both grounded selectors directly, while the same
report still shows the nine excluded CargoTypes-only industrial names that sit outside the
71-row price strip
2026-04-17 08:50:35 -07:00
- the add-building strip `503..519` is now explicitly classified as recovered shell-owned parity,
with tracked fixture coverage, instead of generic unresolved descriptor residue
2026-04-15 09:50:58 -07:00
- widen real packed-event executable coverage descriptor by descriptor after identity, target mask,
and normalized effect semantics are all grounded, not just after row framing is parsed
2026-04-15 14:21:12 -07:00
- the first grounded condition-side unlock now exists for negative-sentinel `raw_condition_id = -1`
2026-04-15 18:27:04 -07:00
company scopes, and the first ordinary nonnegative condition batch now executes too: numeric
thresholds for company finance, company track, aggregate territory track, and company-territory
track
2026-04-15 19:15:47 -07:00
- exact named-territory binding now executes too, while named-territory no-match cases remain the
explicit binding blocker frontier
2026-04-15 20:20:25 -07:00
- real descriptors `8` `Economic Status` , `9` `Confiscate All` , and `15` `Retire Train` now join
the executable batch through the same ordinary runtime path, backed by the opaque economic-status
lane and the minimal event-owned train roster
2026-04-15 20:53:35 -07:00
- descriptor `3` `Territory - Allow All` now executes as company-to-territory access rights through
the same ordinary runtime path; shell purchase-flow parity remains out of scope, and mixed
supported/unsupported real rows still stay parity-only
2026-04-15 21:41:40 -07:00
- whole-game ordinary-condition execution now exists too: special-condition thresholds,
candidate-availability thresholds, and economic-status-code thresholds now gate imported runtime
records, and the packed-event frontier now reports explicit unmapped world-condition and
world-descriptor buckets
2026-04-15 22:38:13 -07:00
- that whole-game condition batch is now metadata-driven too: special-condition label ids,
economic-status, and the generic `%1 Avail.` candidate-availability template plus candidate-name
side strings all decode through checked-in world-condition metadata instead of fixture-only ids
2026-04-15 22:19:09 -07:00
- the first real whole-game grouped-descriptor batch is now metadata-driven too: checked-in
2026-04-15 23:03:18 -07:00
descriptor metadata covers special-condition and candidate-availability setters, and descriptor
`110` `Disable Stock Buying and Selling` now executes too through the checked-in keyed runtime
flag `world.disable_stock_buying_and_selling`
2026-04-16 08:28:50 -07:00
- that world-toggle path now covers a broader recovered boolean scenario-rule band too:
2026-04-16 09:20:49 -07:00
descriptors `111..138` now decode through checked-in metadata into either keyed `world_flags`
or the bounded `world_restore.limited_track_building_amount` scalar for finance/trading,
construction, and governance restrictions
2026-04-16 08:28:50 -07:00
- the late recovered world-toggle band now executes too where current evidence is equally strong:
`Use Bio-Accelerator Cars` , `Disable Cargo Economy` , `Disable Train Crashes` ,
`Disable Train Crashes AND Breakdowns` , and `AI Ignore Territories At Startup`
- whole-game ordinary-condition coverage is broader now too: checked-in world-flag condition ids
can lower into `world_flag_equals` gates for boolean equality/inequality forms, so real packed
rows can gate whole-game effects on existing `world_flags`
2026-04-16 09:27:47 -07:00
- the tracked parity save-slice now keeps its remaining non-imported residue as structured
2026-04-16 22:26:23 -07:00
`real_packed_v1` parity records, with the first captured leftover now pinned to the unresolved
upper locomotives-page availability tail and moved onto explicit descriptor parity
2026-04-16 09:55:58 -07:00
- the next recovered locomotives-page descriptor batch is partially executable too:
descriptors `454..456` (`All Steam/Diesel/Electric Locos Avail.` ) now lower through checked-in
2026-04-16 11:02:21 -07:00
metadata into keyed `world_flags` , while the wider locomotive availability/cost scalar bands now
2026-04-16 22:26:23 -07:00
split cleanly between the grounded lower executable prefix and the explicit unresolved tails
2026-04-16 12:18:13 -07:00
- raw `.smp` inspection/export now reconstructs the persisted save-side named locomotive table and
derives a minimal locomotive catalog from its row order, so save-slice documents can carry both
`RuntimeState.named_locomotive_availability` and the catalog context needed for descriptor
lowering
- recovered scalar locomotive availability and locomotive-cost descriptors now import through that
save-native or embedded `RuntimeState.locomotive_catalog` context into the ordinary
`named_locomotive_availability` and `named_locomotive_cost` runtime maps
2026-04-17 08:18:34 -07:00
- the grounded executable lower locomotive prefix now extends through save-backed locomotive id
`61` (`Zephyr` ); the unresolved lower tail and upper locomotive bands stay on explicit parity
2026-04-16 11:39:59 -07:00
- cargo-production `230..240` and territory-access-cost `453` now execute too through minimal
world-side scalar landing surfaces: slot-indexed `cargo_production_overrides` and
`world_restore.territory_access_cost`
2026-04-16 14:47:38 -07:00
- recipe-book probing now derives a save-native `cargo_catalog` too, so save-slice documents carry
stable cargo slot labels and token-stem evidence into runtime state without requiring a separate
cargo simulation layer
2026-04-16 13:48:55 -07:00
- world-scalar ordinary-condition coverage now matches those runtime surfaces too: checked-in
2026-04-16 14:23:43 -07:00
metadata lowers named locomotive availability, named locomotive cost, named cargo-production
2026-04-16 15:26:37 -07:00
slot thresholds, aggregate cargo production, factory/farm-mine/other cargo production,
limited-track-building-amount, and territory-access-cost rows into explicit runtime condition
gates
- cargo slot classification is now checked in and save-native too, so the remaining cargo frontier
is broader descriptor/condition breadth rather than classification or save/import plumbing
2026-04-16 17:08:03 -07:00
- the company/chairman frontier has moved too: checked-in save-slice documents can now carry that
context natively, so the next work on that axis is broader recovery and eventual raw save
reconstruction rather than overlay-only ownership
2026-04-14 23:01:18 -07:00
- keep in mind that the current local `.gms` corpus still exports with no packed event collection,
so real descriptor mapping needs to stay plumbing-first until better captures exist
2026-04-14 19:37:53 -07:00
- use `rrt-hook` primarily as optional capture or integration tooling, not as the first execution
environment
- keep `docs/runtime-rehost-plan.md` current as the runtime baseline and next implementation slice
change
2026-04-02 23:11:15 -07:00
Regenerate the initial exports with:
```bash
python3 tools/py/collect_pe_artifacts.py \
rt3_wineprefix/drive_c/rt3/RT3.exe \
artifacts/exports/rt3-1.06
```
Regenerate the startup-focused Ghidra exports with:
```bash
python3 tools/py/export_startup_map.py \
rt3_wineprefix/drive_c/rt3/RT3.exe \
artifacts/exports/rt3-1.06
```
2026-04-16 19:03:07 -07:00
Regenerate the checked-in `EventEffects` table export with:
```bash
python3 tools/py/extract_event_effects.py \
rt3_wineprefix/drive_c/rt3/RT3.exe \
rt3_wineprefix/drive_c/rt3/Data/Language/RT3.lng \
artifacts/exports/rt3-1.06/event-effects-table.json
```
2026-04-16 20:20:41 -07:00
Regenerate the checked-in `EventEffects` semantic catalog with:
```bash
python3 tools/py/build_event_effect_semantic_catalog.py \
artifacts/exports/rt3-1.06/event-effects-table.json \
artifacts/exports/rt3-1.06/event-effects-semantic-catalog.json
```
2026-04-02 23:11:15 -07:00
That default export now walks two roots:
- `entry:0x005a313b`
- `bootstrap:0x00484440`
For a focused branch-deepening pass, regenerate the analysis context exports with:
```bash
python3 tools/py/export_analysis_context.py \
rt3_wineprefix/drive_c/rt3/RT3.exe \
artifacts/exports/rt3-1.06 \
--addr 0x00444dd0 \
--addr 0x00508730 \
--addr 0x00508880 \
--string gpdLabelDB \
--string gpdCityDB \
--string 2DLabel.imb \
--string 2DCity.imb \
--string "Geographic Labels"
```
For the pending-template dispatch-store branch, regenerate the new branch dossier with:
```bash
python3 tools/py/rt3_rekit.py \
pending-template-store \
rt3_wineprefix/drive_c/rt3/RT3.exe \
artifacts/exports/rt3-1.06
```
That dossier is now a targeted follow-up tool, not the default first pass.