Analysis and reimplementation of Railroad Tycoon 3


The old executable is at ./rt3_wineprefix/drive_c/rt3/RT3.exe

Our first task is to understand the executable's high-level control loops and subsystem boundaries well
enough to choose good rewrite targets. As we go, we document evidence, keep a curated function map,
and stand up Rust tooling that can validate artifacts and later host replacement code.

The long-term direction is still a DLL we can inject into the original executable, patching in
individual functions as we build them out. The active implementation milestone is now a headless
runtime rehost layer that can execute deterministic world work, compare normalized state, and grow
subsystem breadth without depending on the shell or presentation path. The current packed-event
frontier is broader real grouped-descriptor coverage on top of the existing save-slice, snapshot,
overlay-import, compact-control, and symbolic company-target workflows. The runtime already carries
selected-company and controller-role context through overlay imports, and real descriptors `2`
`Company Cash`, `13` `Deactivate Company`, and `16` `Company Track Pieces Buildable` now parse and
execute through the ordinary runtime path, and descriptors `1` `Player Cash` and `14`
`Deactivate Player` now join that batch through the same service engine. Synthetic packed records
still exercise the same runtime without a parallel packed executor. The first grounded
chairman-profile runtime slice now exists too: save-slice or overlay-backed chairman/company
context plus the hidden grouped target-subject lane let those same real descriptors `1` and `14`
execute on the grounded chairman scope ordinals `0..3` (`condition_true`, `selected`, `human`,
`ai`), while wider chairman ordinals remain explicit parity. The first grounded
chairman and governance condition batch is broader now: selected-chairman cash / holdings / net
worth / purchasing-power thresholds and company book-value-per-share / investor-confidence /
management-attitude thresholds now import through the normal event-service path, while wider
chairman ordinals remain explicit frontier. Checked-in save-slice
documents can now also carry explicit company rosters and chairman-profile tables, so the current
company-targeted and chairman-targeted descriptor and condition batches can execute from standalone
save-slice fixtures without overlay snapshots when that context is present; raw `.gms` inspection
now reconstructs both collections automatically: the fixed save-side `0x32c8` world block still
supplies selected company/chairman ids plus the campaign override byte, the grounded issue-`0x37`
value/multiplier pair, and chairman slot/role-gate analysis bytes, and the tagged company /
chairman-profile direct-record families now populate
save-native roster entries for real `.gms` imports and exports. The current raw-save boundary is
narrower now: company/chairman identity, active flags, links, chairman cash, chairman holdings,
chairman purchasing power, company debt, and company track-laying capacity are grounded directly
from save records, while
broader company finance/governance scalars and controller-kind reconstruction still remain
conservative defaults until their raw lanes are pinned more strongly. The offline runtime analysis
surface also now exposes `runtime inspect-save-company-chairman <save.gms>` for those remaining raw
company/chairman scalar candidates, including fixed-world chairman slot / role-gate context,
explicit company dword candidate windows, richer chairman qword cache views, and derived
holdings-at-share-price / cached purchasing-power comparisons. The same fixed `0x32c8` world
block is now probed for the grounded issue-`0x37` pair at `[world+0x29/+0x2d]`, one broader
fixed-dword finance neighborhood around the absolute-calendar and issue lanes, and the separate
six-float economic tuning band, but current atlas evidence still keeps that editor-facing
tuning family distinct from the governance issue lanes behind investor confidence and prime-rate
math. The next shared company-side slice is now rehosted too: save-native company direct records
flow into a typed company market/cache map on runtime service state, carrying outstanding shares,
saved support/share-price/cache words, chairman salary lanes, calendar words, and connection
latches for each live company. That map now appears in runtime summaries and save-slice exports,
and it now also carries the first grounded stat-band root windows at `[company+0x0cfb]`,
`[company+0x0d7f]`, and `[company+0x1c47]`, so later company stat-family / finance readers can
build on owned state instead of another round of single-field save-offset guesses. The first
runtime-side `0x2329` stat-family reader seam is now rehosted too for the currently grounded slots
`0x0d` (`current_cash`) and `0x1d` (`book_value_per_share`), so later annual-finance logic can
extend one shared reader family instead of hard-coding more direct field accesses. Those saved
stat-band windows are now widened to 16 dwords per root in save-slice/runtime state so later
year-series finance closure has enough owned raw state to attach to. The matching world-side issue
reader seam is now also rehosted for the grounded `0x37` investor-confidence lane on top of the
save-native world-restore state. The selected-company summary path now also exposes the
unassigned share pool derived from outstanding shares minus chairman-held shares, so later
dividend / stock-capital logic can extend one owned market reader instead of another ad hoc
counter. The next bundled annual-finance reader seam is now rehosted on top of that same market
state too, deriving assigned shares, public float, and rounded cached share price from one shared
company market reader instead of scattering more finance helpers across the runtime. A checked-in
The fixed-world finance neighborhood itself is now widened to 16 dwords rooted at `[world+0x11]`,
so future issue-`0x38/0x39` closure can build on a broader owned restore-state window rather than
another narrow one-off probe. A checked-in
The working rule on the remaining frontier is explicit now too: when a lane is still ambiguous, we
should prefer rehosting the owning source state or the real reader/setter family rather than
guessing one more derived leaf field from nearby offsets. A checked-in
`EventEffects` export now exists too in
`artifacts/exports/rt3-1.06/event-effects-table.json`, and a checked-in semantic closure layer now
exists beside it in `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 anonymous descriptor residue. The
first recovered governance descriptor tranche now imports through the generic
company-governance scalar effect surface:
descriptor `56` `Credit Rating` and descriptor `57` `Prime Rate` execute from ordinary real packed
rows, while adjacent recovered finance/control-transfer descriptors such as `55` `Stock Prices`
and `58` `Merger Premium` now land on explicit shell-owned parity instead of anonymous unmapped
descriptor residue, and tracked shell-owned fixtures now pin finance, scenario-outcome, and
control-transfer shell rows explicitly. The
recovered whole-game scalar economy/performance strip `59..104` now has a
bounded runtime landing surface too: representative descriptors import into
`RuntimeState.world_scalar_overrides` through stable normalized keys such as
`world.build_stations_cost`, `world.track_maintenance_cost`, `world.all_engine_speeds`, and
`world.hotel_revenue`. The runtime-variable strip `39..54` now executes too through bounded
event-owned scalar maps on world/company/player/territory state, and the matching ordinary
condition strip now gates records through those same maps too, without widening save-native
reconstruction or adding a second packed executor. The grounded aggregate cargo-economics
descriptors now have bounded
runtime landing surfaces too: descriptor `105` `All Cargo Prices` plus descriptors `177..179`
`All Cargo Production` / `All Factory Production` / `All Farm/Mine Production` import into
event-owned cargo override state, and the grounded named cargo-production strip `180..229` now
imports into named cargo production overrides too, and the named cargo-price strip `106..176` now
imports into named cargo price overrides as well. The checked-in static selector reconstruction is
now explicit: 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 named price strip as
`cargoSkin` plus the core `Rock` carry-over. The checked-in
`artifacts/exports/rt3-1.06/economy-cargo-sources.json` report parses both `CargoTypes` and the
`Cargo106.PK4` `cargoSkin` descriptors, normalizes localized `~####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 makes the residual live-registry gap
explicit by showing the nine excluded CargoTypes-only industrial names outside the 71-row price
strip. The
add-building strip `503..519` is now explicitly classified as recovered
shell-owned descriptor parity rather than generic unresolved residue. The first grounded
condition-side unlock now exists for negative-sentinel `raw_condition_id = -1` company scopes, and
the first ordinary nonnegative condition batch now executes too: numeric-threshold company
finance, company track, aggregate territory track, and company-territory track rows can import
through overlay-backed runtime context. Exact named-territory binding now executes, and the runtime
now also carries the minimal event-owned train roster and opaque economic-status lane needed for
real descriptors `8` `Economic Status`, `9` `Confiscate All`, and `15` `Retire Train` to execute
through the same path. Descriptor `3`
`Territory - Allow All` now executes too, reinterpreted as company-to-territory access rights
rather than a territory-owned policy bit. Whole-game ordinary-condition execution now exists too:
special-condition thresholds, candidate-availability thresholds, and economic-status-code
thresholds now gate imported runtime records through the same service path, and that world-side
condition batch now decodes from checked-in metadata instead of fixture-only ids: real
special-condition label ids, real economic-status ids, and the recovered `%1 Avail.` candidate
template plus candidate-name side strings all lower into the runtime condition model. Checked-in
whole-game descriptor metadata now drives the first real world-side effect batch too:
special-condition and candidate-availability setters import natively, and descriptor `110`
`Disable Stock Buying and Selling` now lowers into the keyed runtime flag
`world.disable_stock_buying_and_selling`. The recovered whole-game toggle batch is broader now
too: descriptors `111..138`, with descriptor `122` `Limited Track Building Amount` now landing in
the bounded `world_restore.limited_track_building_amount` scalar and the remaining boolean lanes
lowering into keyed `world_flags`, cover finance/trading, construction, and governance
restrictions. Explicit the late recovered special-condition toggles now execute 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 condition decode
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 records can gate whole-game effects on
existing `world_flags` without fixture-authored placeholder ids. The tracked parity save-slice no
longer depends on a raw `unsupported_framing` placeholder either: its remaining residue is now one
recovered locomotives-page `real_packed_v1` record that now lands on explicit descriptor parity
instead of a generic unmapped bucket. The next recovered descriptor band is now partially
executable too: descriptors `454..456` (`All Steam/Diesel/Electric Locos Avail.`) now lower
through checked-in metadata into keyed `world_flags`, while the wider locomotive availability/cost
scalar bands are now save-native too. Raw `.smp` inspection/export reconstructs the persisted
`[world+0x66b6]` locomotive name table and derives a minimal `RuntimeState.locomotive_catalog`, so
standalone save-slice imports can now lower the grounded lower locomotive availability and
locomotive-cost rows directly into `RuntimeState.named_locomotive_availability` and
`RuntimeState.named_locomotive_cost` without needing overlay snapshots when the save carries enough
catalog context, and the grounded executable lower prefix now extends through save-backed
locomotive id `61` (`Zephyr`); the unresolved lower tail and upper locomotive bands now stay on
explicit parity instead of synthetic execution. The remaining recovered scalar world families
execute too:
cargo-production slots `230..240` lower into `cargo_production_overrides`, and descriptor `453`
lowers into
`world_restore.territory_access_cost`. Whole-game ordinary-condition breadth now aligns with those
same world-scalar runtime surfaces too: named locomotive availability thresholds, named
locomotive cost thresholds, named cargo-production slot thresholds, aggregate cargo-production
thresholds, factory/farm-mine/other cargo-production thresholds, limited-track-building-amount
thresholds, and territory-access-cost thresholds all gate imported runtime records through the
same service path. Explicit unmapped world-condition frontier buckets still remain where current
checked-in metadata stops, and
`blocked_missing_locomotive_catalog_context` is now reserved for intentionally incomplete save-side
catalog context instead of the normal save-slice path. Cargo slot identity and class metadata are
now save-native too: the recipe-book probe lowers into `RuntimeState.cargo_catalog`, so save-slice
documents can carry slot labels, class tags, and token-stem evidence alongside the executable
`cargo_production_overrides` surface without introducing a live cargo-economy model. Shell
purchase-flow, Trainbuy refresh,
cached locomotive-rating recomputation, and selected-profile parity remain out of scope. Mixed
supported/unsupported real rows still stay parity-only. The PE32 hook remains useful as capture and
integration tooling, but it is no longer the main execution milestone.

## Project Docs

Bootstrap design and workflow documents live in `docs/`.

- `docs/README.md`: handbook index and target hashes
- `docs/control-loop-atlas.md`: compatibility index for the split atlas
- `docs/control-loop-atlas/`: canonical atlas section files
- `docs/setup-workstation.md`: toolchain baseline and local setup
- `docs/re-workflow.md`: repeatable reverse-engineering workflow
- `docs/function-map.md`: canonical function-map schema and conventions

The first committed exports for the canonical 1.06 executable live in `artifacts/exports/rt3-1.06/`.

## Rust Workspace

The Rust workspace is split into focused crates:

- `rrt-model`: shared types for addresses, function-map rows, and control-loop concepts
- `rrt-runtime`: headless runtime state, stepping, normalized event service, and persistence-facing
  runtime types
- `rrt-fixtures`: fixture schemas, loading, normalization, and diff helpers for rehost validation
- `rrt-cli`: validation, runtime fixture execution, state-diff tools, and repo-health checks
- `rrt-hook`: minimal Windows DLL scaffold for low-risk in-process loading, capture, and later
  integration experiments under Wine

For the current headless runtime smoke path, use `cargo run -p rrt-cli -- runtime summarize-fixture
fixtures/runtime/minimal-world-step-smoke.json` or one of the broader runtime fixtures under
`fixtures/runtime/`.

For the current hook smoke test, run `tools/run_hook_smoke_test.sh`. It builds the PE32 proxy,
copies it into the local RT3 install, launches the game briefly under Wine with
`WINEDLLOVERRIDES=dinput8=n,b`, and expects `rrt_hook_attach.log` to appear.
