124 lines
9.2 KiB
Markdown
124 lines
9.2 KiB
Markdown
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
|
|
still does not reconstruct those company/chairman collections automatically. A generic
|
|
company-governance scalar effect surface now exists in runtime too, but real governance descriptor
|
|
ids are still deferred until the checked-in effect-table evidence is stronger. 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 lands in the explicit
|
|
`blocked_unmapped_world_descriptor` 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 recovered 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. 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 and world-descriptor
|
|
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.
|