No description
  • Rust 97.8%
  • Python 1.8%
  • Java 0.3%
Find a file
2026-04-18 13:09:52 -07:00
.cargo Build RE baseline and initial Rust workspace 2026-04-02 23:11:15 -07:00
artifacts Rehost selected-year bucket companion bands 2026-04-18 07:06:30 -07:00
crates Narrow region restore handoff to vtable callback 2026-04-18 13:09:52 -07:00
docs Narrow region restore handoff to vtable callback 2026-04-18 13:09:52 -07:00
fixtures/runtime Carry fixed-world finance neighborhood into runtime state 2026-04-17 20:26:29 -07:00
tools Rehost selected-year bucket companion bands 2026-04-18 07:06:30 -07:00
.gitignore Build RE baseline and initial Rust workspace 2026-04-02 23:11:15 -07:00
Cargo.lock Add headless runtime tooling and Campaign.win analysis 2026-04-10 01:22:47 -07:00
Cargo.toml Add headless runtime tooling and Campaign.win analysis 2026-04-10 01:22:47 -07:00
README.md Rank higher-layer consumer hypotheses 2026-04-18 12:53:44 -07:00
RT2.LOG Commit runtime loader and atlas updates 2026-04-11 18:12:25 -07:00
rt3_auto_load_winedbg.log Commit runtime loader and atlas updates 2026-04-11 18:12:25 -07:00
rt3_manual_load_winedbg.log Commit runtime loader and atlas updates 2026-04-11 18:12:25 -07:00

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], and the adjacent raw issue-byte strip 0x37..0x3a now also flows through save-slice/runtime restore state as first-class owner data for later credit / prime-rate / management-attitude readers. One broader fixed-dword finance neighborhood rooted at [world+0x0d] that now carries the saved calendar tuple and absolute-counter owner lanes directly, 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 32 dwords per root in save-slice/runtime state so later year-series finance closure has a broader owned raw state band 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 17 dwords rooted at [world+0x0d], so later finance closure can build on a broader owned restore-state window rather than another narrow one-off probe; that same owner surface now also carries the saved absolute counter as first-class runtime restore state instead of leaving it on “requires shell context” metadata. The same save-world owner surface now also carries the packed year word and partial-year progress lane behind the annual-finance recent-history weighting path, so later finance readers can attach to real world-calendar state instead of candidate bytes. The next company-side seam is now bundled too: a shared company market reader now exposes outstanding shares, assigned shares, public float, rounded cached share price, salary lanes, bonus amount, and the full two-word current/prior issue-calendar tuples from the owned annual-finance state instead of leaving that logic spread across summary helpers. The same annual-finance state now also derives elapsed years since founding, last dividend, and last bankruptcy from the runtime calendar, which lines up directly with the grounded annual finance-policy gates in the atlas. Live bond-slot count is now carried through the same owned company market and annual-finance state too, which matches the stock-capital branch gate that requires at least two live bonds. The same grounded bond table now also contributes both the largest live bond principal and the chosen highest-coupon live bond principal into owned company market and annual-finance state, so the stock-capital approval ladder can extend one rehosted owner-state surface instead of hunting another isolated finance leaf. The same bond-slot owner state now also exposes the highest live coupon rate, which is enough to run the stock-capital price-to-book approval ladder as another save-native runtime reader instead of a notes-only threshold table. A checked-in fixed-world finance-policy seam now also carries the raw stock, bond, bankruptcy, and dividend policy bytes from the 0x32c8 save block, and the first annual creditor-pressure branch now runs headlessly as a pure runtime reader over owned annual-finance state, support-adjusted share price, and current world finance policy rather than as a notes-only atlas fragment. The later deep- distress bankruptcy fallback is now rehosted on that same owner surface too, using the save-native cash reader seam plus the first three trailing net-profit years instead of another ad hoc probe. The annual bond lane now runs on that same owner surface too, using the simulated post-repayment cash window plus the linked-transit threshold split to stage 500000 principal issue counts as a pure runtime reader, and periodic boundary service now commits the same shellless matured-bond repayment-and- compaction path before issuing the exact staged count. The annual dividend lane now runs there too: the runtime now rehosts the shared year-or-control-transfer metric seam, the board-approved dividend ceiling helper, and the full annual dividend adjustment branch over owned current cash, public float, current dividend, building-growth policy, and recent profit history instead of leaving that policy on shell-side dialog notes. The same periodic service now also carries the annual bond lane's retired-versus- issued principal totals as first-class runtime summary state, which is the owner seam behind the later debt-news family, and it now carries the paired issued-share and repurchased-share counts behind the equity-offering and 2887 buyback news tails too. Runtime summaries now also expose the grounded retired-versus-issued relation directly, and annual finance service now maps that same comparison onto the exact debt headline selectors 2882..2886. simulation_service_periodic_boundary_work is now beginning to use that same owner surface too: the runtime chooses one annual-finance action per active company and already commits the shellless creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase, stock-issue, and bond-issue branches by mutating owned company activity, dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot state instead of stopping at reader-only diagnostics. That same service state now also persists the last emitted annual-finance news events as structured runtime records carrying company id, exact selector label, action label, and the grounded debt/share payload totals used by the shell news layer. Calendar stepping now also starts to use that same seam directly: StepCount and AdvanceTo invoke the periodic-boundary service automatically on year rollover, so shellless calendar advance can drive the annual finance stack instead of requiring a separate manual service command. That stepped world-time path now also refreshes the rehosted selected-year gap scalar owner lane instead of leaving [world+0x4ca2] as a frozen load-time residue. The same save-native world restore surface now also carries the grounded locomotive-policy bytes and cached available-locomotive rating from the fixed world block, so the All Steam/Diesel/Electric Locos Avail. descriptor strip now writes through owner state instead of living only as ad hoc world flags. The selected-year seam is now doing the same thing: the checked-in 0x00433bd0 year ladder now drives a derived selected-year bucket scalar in runtime restore state, and the economic-tuning mirror [world+0x0bde] now rebuilds from tuning lane 0 instead of freezing one stale load-time word. That same checked-in owner family now also rebuilds the direct bucket trio [world+0x65/+0x69/+0x6d], the complement trio [world+0x71/+0x75/+0x79], and the scaled companion trio [world+0x7d/+0x81/+0x85] from the selected-year bucket scalar instead of preserving stale save-time residue. Those bankruptcy branches now follow the grounded owner semantics too: they stamp the bankruptcy year and halve live bond principals in place instead of treating bankruptcy as a liquidation path. The same save-native live bond-slot surface now also carries per-slot maturity years all the way through runtime summaries and annual bond policy state, which is the next owner seam needed for shellless repayment and bond-burden simulation instead of another round of raw-slot guessing. The same save-native company direct-record seam now also carries the full outer periodic-company side-latch trio rooted at 0x0d17/0x0d18/0x0d56, including the preferred-locomotive engine-type chooser byte that sits beside the city-connection and linked-transit finance gates. That same seam now also resolves the base world route-preference byte at [world+0x4c74], the effective electric-only override fed by 0x0d17, and the matching 1.4x versus 1.8x route-quality multiplier as a normal runtime reader instead of leaving that bridge in atlas notes. That same seam now also owns the first route-preference mutation path directly: beginning the electric-only periodic-company override rewrites the world route-preference byte to the effective company preference, ending it restores the base world byte, and runtime service state now carries both the active and last applied override instead of treating the route-preference lane as a reader-only bridge. Save inspection now also separates the shared 0x5209/0x520a/0x520b save family correctly: the smaller direct 0x1d5 collection is the live train family and now exposes a live-entry directory rooted at metadata dword 16, while the actual region collection is the larger non-direct Marker09 family. The tagged placed-structure header 0x36b1/0x36b2/0x36b3 is grounded alongside them, so the remaining city-connection / linked-transit blocker is record-body reconstruction rather than missing save-side collection identity. That same seam now also derives the current live coupon burden directly from owned bond slots, so later finance service work can consume a runtime reader instead of recomputing from scattered raw fields. The same seam now also carries the fixed-world building-density growth setting plus the linked chairman personality byte, which is enough to run the annual stock-repurchase gate as another pure reader over owned save-native state instead of a guessed finance-side approximation. 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, and the checked-in docs/rehost-queue.md file is now the control surface for that loop: after each commit, check the queue and continue unless the queue is empty, a real blocker remains that cannot be advanced by targeted static analysis or by expanding the rehosted probe/owner surface without guessing, or approval is needed. A checked-in The same runtime surface now also exposes higher-layer blocker probes: runtime inspect-periodic-company-service-trace <save.gms>, runtime inspect-region-service-trace <save.gms>, and runtime inspect-infrastructure-asset-trace <save.gms>, so the next city-connection / linked-transit slices can start from explicit owner-seam blockers instead of another generic save scan. 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.