| .. | ||
| atlas | ||
| control-loop-atlas | ||
| control-loop-atlas.md | ||
| debug-load-workflow.md | ||
| function-map.md | ||
| re-workflow.md | ||
| README.md | ||
| runtime-rehost-plan.md | ||
| setup-workstation.md | ||
| shell-load-graph.md | ||
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.control-loop-atlas.md: compatibility index for the split atlas, preserving legacy anchors.control-loop-atlas/: canonical section files for the atlas narrative.runtime-rehost-plan.md: bottom-up runtime replacement plan and milestone breakdown.
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.csvand the derived branch artifacts underartifacts/exports/rt3-1.06/.
Current local tool status:
- Ghidra is installed at
~/software/ghidra ~/software/ghidra/ghidraRunlaunches successfully in an interactive shell- Rizin is installed and available on
PATH winedbgworks withrt3_wineprefix- RT3 launches under
/opt/wine-stable/bin/winewhen started fromrt3_wineprefix/drive_c/rt3
Next Focus
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 trigger dispatch, normalized runtime effects, staged event-record mutation, fixture execution, 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.
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
- keep using overlay imports as the context bridge when selectively executable packed rows still need runtime context that current save slices and raw save inspection do not yet persist
- 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
2Company Cash, descriptor13Deactivate Company, and descriptor16Company Track Pieces Buildable - descriptors
1Player Cashand14Deactivate Playernow join that executable real batch through the same ordinary runtime path, backed by the minimal player runtime and overlay-import context - the first chairman-targeted real grouped rows now execute too through that same path when the
hidden grouped target-subject lane resolves to grounded chairman scope ordinals
0..3:condition_true_chairman,selected_chairman,human_chairmen, andai_chairmen; wider chairman ordinals stay parity-only underblocked_chairman_target_scope - 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 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
- 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
present; raw
.gmsinspection/export now reconstructs those company/chairman collections directly from save-side tagged record families, while the fixed save-side0x32c8world block still provides selected company/chairman ids plus the grounded campaign-override, issue-0x37value/multiplier, and chairman-slot / role-gate analysis bytes; the current raw-save frontier is narrower now: 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 finance/governance scalar lanes plus controller-kind reconstruction still remain conservative defaults until their raw offsets are pinned more strongly; the offline analysis commandruntime inspect-save-company-chairman <save.gms>now dumps those remaining raw record candidates directly from the rehosted parser, including fixed-world chairman slot / role-gate context, the grounded fixed-world issue-0x37pair, the fixed-dword world finance neighborhood around the absolute-calendar and issue lanes, the separate six-float economic tuning band, derived holdings-at-share-price and cached purchasing-power totals, 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 - 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
- a checked-in
EventEffectsexport now exists atartifacts/exports/rt3-1.06/event-effects-table.json, and a checked-in semantic closure layer now exists atartifacts/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, andblocked_variant_or_scope_blocked_descriptorinstead of generic unmapped-descriptor residue - the first recovered governance descriptor tranche now executes through the generic
company-governance scalar effect surface: descriptor
56Credit Ratingand descriptor57Prime Rate - adjacent recovered finance/control-transfer descriptors such as
55Stock Pricesand58Merger Premiumnow land on explicit shell-owned descriptor parity instead of generic unmapped descriptor residue, with tracked fixtures now pinning finance, scenario-outcome, and control-transfer shell rows explicitly - the recovered whole-game scalar economy/performance strip
59..104now has a bounded runtime landing surface too: representative rows execute intoRuntimeState.world_scalar_overridesthrough stable normalized keys such asworld.build_stations_costandworld.track_maintenance_cost - the runtime-variable strip
39..54now executes too through bounded event-owned scalar maps on 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 - the grounded aggregate cargo-economics descriptors now execute too: descriptor
105All Cargo Pricesand descriptors177..179All Cargo Production/All Factory Production/All Farm/Mine Productionland on bounded event-owned cargo override state, and the grounded named cargo-production strip180..229now lands on named cargo production overrides too - the named cargo-price strip
106..176now lands on named cargo price overrides too; the checked-in static selector reconstruction is now explicit because the broader 1.06 CargoTypes corpus has51names, the Cargo106cargoSkincorpus has70, and the rehosted offline selector builder now closes the 71-row price strip ascargoSkinplus the coreRockcarry-over; a new checked-in offline cargo-source report atartifacts/exports/rt3-1.06/economy-cargo-sources.jsonnow parses bothCargoTypesand theCargo106.PK4cargoSkindescriptors through rehosted code, normalizes localized~####Nametokens 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 - the add-building strip
503..519is now explicitly classified as recovered shell-owned parity, with tracked fixture coverage, instead of generic unresolved descriptor residue - 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
- the first grounded condition-side unlock now exists for negative-sentinel
raw_condition_id = -1company 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 - exact named-territory binding now executes too, while named-territory no-match cases remain the explicit binding blocker frontier
- real descriptors
8Economic Status,9Confiscate All, and15Retire Trainnow join the executable batch through the same ordinary runtime path, backed by the opaque economic-status lane and the minimal event-owned train roster - descriptor
3Territory - Allow Allnow 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 - 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
- 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 - the first real whole-game grouped-descriptor batch is now metadata-driven too: checked-in
descriptor metadata covers special-condition and candidate-availability setters, and descriptor
110Disable Stock Buying and Sellingnow executes too through the checked-in keyed runtime flagworld.disable_stock_buying_and_selling - that world-toggle path now covers a broader recovered boolean scenario-rule band too:
descriptors
111..138now decode through checked-in metadata into either keyedworld_flagsor the boundedworld_restore.limited_track_building_amountscalar for finance/trading, construction, and governance restrictions - 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, andAI Ignore Territories At Startup - whole-game ordinary-condition coverage is broader now too: checked-in world-flag condition ids
can lower into
world_flag_equalsgates for boolean equality/inequality forms, so real packed rows can gate whole-game effects on existingworld_flags - the tracked parity save-slice now keeps its remaining non-imported residue as structured
real_packed_v1parity records, with the first captured leftover now pinned to the unresolved upper locomotives-page availability tail and moved onto explicit descriptor parity - 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 metadata into keyedworld_flags, while the wider locomotive availability/cost scalar bands now split cleanly between the grounded lower executable prefix and the explicit unresolved tails - raw
.smpinspection/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 bothRuntimeState.named_locomotive_availabilityand 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_catalogcontext into the ordinarynamed_locomotive_availabilityandnamed_locomotive_costruntime maps - 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 - cargo-production
230..240and territory-access-cost453now execute too through minimal world-side scalar landing surfaces: slot-indexedcargo_production_overridesandworld_restore.territory_access_cost - recipe-book probing now derives a save-native
cargo_catalogtoo, so save-slice documents carry stable cargo slot labels and token-stem evidence into runtime state without requiring a separate cargo simulation layer - world-scalar ordinary-condition coverage now matches those runtime surfaces too: checked-in metadata lowers named locomotive availability, named locomotive cost, named cargo-production 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
- 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
- keep in mind that the current local
.gmscorpus still exports with no packed event collection, so real descriptor mapping needs to stay plumbing-first until better captures exist - use
rrt-hookprimarily as optional capture or integration tooling, not as the first execution environment - keep
docs/runtime-rehost-plan.mdcurrent as the runtime baseline and next implementation slice change
Regenerate the initial exports with:
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:
python3 tools/py/export_startup_map.py \
rt3_wineprefix/drive_c/rt3/RT3.exe \
artifacts/exports/rt3-1.06
Regenerate the checked-in EventEffects table export with:
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
Regenerate the checked-in EventEffects semantic catalog with:
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
That default export now walks two roots:
entry:0x005a313bbootstrap:0x00484440
For a focused branch-deepening pass, regenerate the analysis context exports with:
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:
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.