Refactor runtime ownership and clean up warnings

This commit is contained in:
Jan Petykiewicz 2026-04-21 15:40:17 -07:00
commit 486b061558
628 changed files with 97954 additions and 90763 deletions

2
.gitignore vendored
View file

@ -4,9 +4,9 @@
/ghidra_projects/
/rt3_wineprefix/
/rt3_wineprefix2/
/artifacts/tmp/
/tools/py/__pycache__/
/.codex
/\ %s
*.gpr
*.rep
*.rzdb

332
README.md
View file

@ -1,309 +1,47 @@
Analysis and reimplementation of Railroad Tycoon 3
Analysis and reimplementation of Railroad Tycoon 3.
## Overview
The old executable is at ./rt3_wineprefix/drive_c/rt3/RT3.exe
This repository supports two parallel tracks:
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.
- reverse-engineering the 1.06 executable into durable atlases, exports, and capture notes
- building a headless Rust runtime that can rehost deterministic world work outside the shell
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`](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 any further non-hook work without guessing, or approval
is needed. `final` responses are stop-only there too: if no stop condition is true, keep working
and use `commentary` updates instead of placeholder status replies. 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.
The canonical executable target is `rt3_wineprefix/drive_c/rt3/RT3.exe`.
## Project Docs
## Workspace
Bootstrap design and workflow documents live in `docs/`.
- `crates/rrt-model`: shared schema, finance logic, and project-level constants
- `crates/rrt-runtime`: headless runtime state, importers, inspectors, stepping, and summaries
- `crates/rrt-fixtures`: fixture loading, normalization, validation, and diff helpers
- `crates/rrt-cli`: validation, runtime inspection, export, and comparison commands
- `crates/rrt-hook`: PE32 hook scaffold for capture and integration experiments
- `docs/`: stable handbook material, atlases, plans, and active queues
- `artifacts/exports/`: committed derived research outputs
- `artifacts/captures/`: committed raw logs, sample binaries, and retained capture evidence
- `fixtures/runtime/`: checked-in runtime fixtures
- `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
## Common Commands
The first committed exports for the canonical 1.06 executable live in `artifacts/exports/rt3-1.06/`.
- `cargo test --workspace`
- `cargo run -p rrt-cli -- validate .`
- `cargo run -p rrt-cli -- runtime summarize-fixture fixtures/runtime/minimal-world-step-smoke.json`
- `cargo run -p rrt-cli -- runtime inspect-smp <save.gms>`
- `tools/run_hook_smoke_test.sh`
## Rust Workspace
## Docs
The Rust workspace is split into focused crates:
- [Handbook](docs/README.md)
- [Runtime Rehost Plan](docs/runtime-rehost-plan.md)
- [Active Rehost Queue](docs/rehost-queue.md)
- [Progress History](docs/history/progress-history.md)
- [Control-Loop Atlas Index](docs/control-loop-atlas/README.md)
- [Subsystem Views](docs/subsystem-views/README.md)
- [1.06 Export Index](artifacts/exports/rt3-1.06/README.md)
- [Capture Index](artifacts/captures/README.md)
- `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
## Notes
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.
- `artifacts/tmp/` is scratch-only and should stay untracked.
- Canonical checked-in captures belong under `artifacts/captures/`.
- Detailed running status belongs in the docs and artifact indexes above, not in this root README.

View file

@ -0,0 +1,14 @@
# Capture Index
Committed raw capture evidence and retained sample binaries live here.
## Layout
- `logs/`: retained hook and runtime logs that are part of the project record
- `win-bin/`: preserved raw `.win` sample binaries
- `analysis/`: retained analysis-side capture snapshots that are still referenced
## Policy
- Add files here only when they are durable evidence worth checking in.
- Put temporary local experiments under `artifacts/tmp/` instead.

View file

@ -0,0 +1,49 @@
# RT3 1.06 Export Index
Canonical derived outputs for the patch 1.06 executable.
## Baseline Binary Facts
- `binary-summary.json`
- `sections.csv`
- `imported-dlls.txt`
- `imported-functions.csv`
- `interesting-strings.txt`
- `startup-call-chain.md`
- `ghidra-startup-functions.csv`
- `subsystem-inventory.md`
## Function and Analysis Maps
- `function-map.csv`
- `analysis-context.md`
- `analysis-context-functions.csv`
- `analysis-context-strings.csv`
- `pending-template-store-functions.csv`
- `pending-template-store-record-kinds.csv`
- `pending-template-store-management.md`
## Event and Data Model Exports
- `event-effects-table.json`
- `event-effects-semantic-catalog.json`
- `event-effects-cargo-bindings.json`
- `event-effects-building-bindings.json`
- `economy-cargo-sources.json`
- `building-type-sources.json`
- `selected-year-bucket-ladder.json`
## Subgraphs and Branch Notes
- `shell-load-subgraph.*`
- `setup-window-subgraph.*`
- `setup-window-submodes-depth5-*`
- `runtime-effect-service-depth7-*`
- `runtime-effect-kind8-*`
- `world-entry-bringup-refresh-*`
- `world-load-saved-runtime-state-*`
## Notes
- Files here are committed canonical exports or preserved branch dossiers.
- Scratch analysis output belongs under `artifacts/tmp/` and should stay untracked.

File diff suppressed because one or more lines are too long

View file

@ -25,7 +25,7 @@ digraph shell_load {
label="map";
color="#cccccc";
"0x004010f0" [label="0x004010f0\\ncity_compute_connection_bonus_candidate_weight", fillcolor="#f8f8f8"];
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_with_optional_direct_site_placement", fillcolor="#f8f8f8"];
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_and_optionally_place_direct_site", fillcolor="#f8f8f8"];
"0x00404640" [label="0x00404640\\ncity_connection_bonus_try_compact_route_builder_from_region_entry", fillcolor="#f8f8f8"];
"0x004046a0" [label="0x004046a0\\ncity_connection_bonus_build_peer_route_candidate", fillcolor="#f8f8f8"];
"0x00404c60" [label="0x00404c60\\ncity_connection_try_build_route_between_region_entry_pair", fillcolor="#f8f8f8"];
@ -598,7 +598,7 @@ digraph shell_load {
label="simulation";
color="#cccccc";
"0x004014b0" [label="0x004014b0\\ncompany_try_buy_unowned_industry_near_city_and_publish_news", fillcolor="#f8f8f8"];
"0x004019e0" [label="0x004019e0\\ncompany_service_periodic_city_connection_finance_and_linked_transit_lanes", fillcolor="#f8f8f8"];
"0x004019e0" [label="0x004019e0\\ncompany_service_periodic_city_connection_finance_and_linked_transit", fillcolor="#f8f8f8"];
"0x00401c50" [label="0x00401c50\\ncompany_evaluate_annual_finance_policy_and_publish_news", fillcolor="#f8f8f8"];
"0x00404ce0" [label="0x00404ce0\\nsimulation_try_select_and_publish_company_start_or_city_connection_news", fillcolor="#f8f8f8"];
"0x00406050" [label="0x00406050\\ncompany_evaluate_and_publish_city_connection_bonus_news", fillcolor="#f8f8f8"];

View file

@ -11,9 +11,9 @@
| --- | --- | --- | --- |
| `0x004010f0` | `city_compute_connection_bonus_candidate_weight` | `map` | `4` |
| `0x004014b0` | `company_try_buy_unowned_industry_near_city_and_publish_news` | `simulation` | `2` |
| `0x004019e0` | `company_service_periodic_city_connection_finance_and_linked_transit_lanes` | `simulation` | `2` |
| `0x004019e0` | `company_service_periodic_city_connection_finance_and_linked_transit` | `simulation` | `2` |
| `0x00401c50` | `company_evaluate_annual_finance_policy_and_publish_news` | `simulation` | `2` |
| `0x00402cb0` | `city_connection_try_build_route_with_optional_direct_site_placement` | `map` | `3` |
| `0x00402cb0` | `city_connection_try_build_route_and_optionally_place_direct_site` | `map` | `3` |
| `0x00404640` | `city_connection_bonus_try_compact_route_builder_from_region_entry` | `map` | `3` |
| `0x004046a0` | `city_connection_bonus_build_peer_route_candidate` | `map` | `4` |
| `0x00404c60` | `city_connection_try_build_route_between_region_entry_pair` | `map` | `3` |
@ -722,11 +722,11 @@
-> `0x00455800` `runtime_object_query_normalized_primary_coord`
-> `0x00455810` `runtime_object_query_normalized_secondary_coord`
- `0x004014b0` `company_try_buy_unowned_industry_near_city_and_publish_news`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00426590` `company_count_linked_transit_sites`
-> `0x00455f60` `world_region_resolve_center_world_grid_cell`
-> `0x0051c920` `localization_lookup_display_label_by_stem_or_fallback`
- `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
- `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x004014b0` `company_try_buy_unowned_industry_near_city_and_publish_news`
-> `0x00401c50` `company_evaluate_annual_finance_policy_and_publish_news`
-> `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
@ -740,7 +740,7 @@
-> `0x0041d550` `locomotive_era_and_engine_type_pass_company_policy_gate`
-> `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
- `0x00401c50` `company_evaluate_annual_finance_policy_and_publish_news`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00423d70` `company_repay_bond_slot_and_compact_debt_table`
-> `0x00424fd0` `company_compute_public_support_adjusted_share_price_scalar`
-> `0x00425a90` `company_declare_bankruptcy_and_halve_bond_debt`
@ -750,7 +750,7 @@
-> `0x00427450` `company_issue_public_shares_and_raise_capital`
-> `0x004275c0` `company_issue_bond_and_record_terms`
-> `0x0042a5d0` `company_read_year_or_control_transfer_metric_value`
- `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
- `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
-> `0x00404c60` `city_connection_try_build_route_between_region_entry_pair`
@ -764,10 +764,10 @@
-> `0x004931e0` `route_entry_collection_run_optional_refresh_hooks_and_validate_world_cell_side_lists`
-> `0x004a01a0` `route_entry_collection_try_build_path_between_optional_endpoint_entries`
- `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
- `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x00406050` `company_evaluate_and_publish_city_connection_bonus_news`
-> `0x00420280` `city_connection_bonus_select_first_matching_peer_site`
@ -775,7 +775,7 @@
-> `0x00455810` `runtime_object_query_normalized_secondary_coord`
-> `0x005a10d0` `math_round_st0_to_signed_qword_with_current_x87_mode`
- `0x00404c60` `city_connection_try_build_route_between_region_entry_pair`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
- `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
-> `0x004010f0` `city_compute_connection_bonus_candidate_weight`
@ -798,7 +798,7 @@
-> `0x00455810` `runtime_object_query_normalized_secondary_coord`
-> `0x00482e00` `runtime_query_hundredths_scaled_build_version`
- `0x004078a0` `company_select_preferred_available_locomotive_id`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00409300` `company_publish_train_upgrade_news`
-> `0x00409830` `company_try_add_linked_transit_train_and_publish_news`
-> `0x0041d550` `locomotive_era_and_engine_type_pass_company_policy_gate`
@ -1079,7 +1079,7 @@
- `0x00417790` `map_angle_rotate_grid_offset_pair_into_world_offset_pair`
-> `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
- `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00413d80` `aux_candidate_entry_query_owner_subtype1_grid_basis_pair_words_0xcb_0xcd`
-> `0x00417790` `map_angle_rotate_grid_offset_pair_into_world_offset_pair`
-> `0x004197e0` `placed_structure_validate_projected_candidate_placement`
@ -1112,7 +1112,7 @@
- `0x00419680` `aux_candidate_collection_release_templates_queues_and_indexed_storage`
-> `0x00416950` `aux_candidate_collection_release_live_entries_scratch_roots_and_helper_bands`
- `0x004197e0` `placed_structure_validate_projected_candidate_placement`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00413df0` `projected_rect_packed_cell_list_try_append_unique_xy_with_optional_highbit_flag_and_expand_quarter_bounds`
-> `0x00414bd0` `world_query_float_coords_within_live_grid_bounds`
-> `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
@ -1780,7 +1780,7 @@
-> `0x004423d0` `shell_status_stack_pop_restore_and_service_active_stationplace_and_tracklay_tools`
-> `0x00443a50` `world_entry_transition_and_runtime_bringup`
- `0x004423a0` `shell_status_stack_push_and_service_active_tracklay_and_stationplace_tools`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x004422d0` `shell_status_stack_push_four_shell_byte_latches_and_startup_byte`
-> `0x00446240` `world_runtime_serialize_smp_bundle`
-> `0x0046b9f0` `multiplayer_preview_dataset_service_current_session_buckets_and_publish_selector0x67`
@ -2313,7 +2313,7 @@
- `0x00482d80` `runtime_query_cached_local_exe_version_string`
-> `0x00482d10` `runtime_query_cached_local_exe_version_float`
- `0x00482e00` `runtime_query_hundredths_scaled_build_version`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00482d10` `runtime_query_cached_local_exe_version_float`
-> `0x004a65b0` `aux_route_entry_tracker_dispatch_route_entry_pair_metric_query`
-> `0x005a10d0` `math_round_st0_to_signed_qword_with_current_x87_mode`
@ -2432,8 +2432,8 @@
-> `0x0049c900` `route_entry_collection_try_extend_search_frontier_toward_target_coords`
-> `0x0049d380` `route_entry_collection_search_path_between_entry_or_coord_endpoints`
- `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00494cb0` `route_entry_collection_try_find_route_entry_covering_point_window`
-> `0x0049d380` `route_entry_collection_search_path_between_entry_or_coord_endpoints`
-> `0x004a6630` `aux_route_entry_tracker_query_best_route_entry_pair_metric_with_endpoint_fallbacks`
@ -2455,7 +2455,7 @@
-> `0x0051db80` `math_measure_float_xy_pair_distance`
-> `0x005a152e` `math_abs_double_with_crt_special_case_handling`
- `0x004a01a0` `route_entry_collection_try_build_path_between_optional_endpoint_entries`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00480bb0` `placed_structure_refresh_linked_site_display_name_and_route_anchor`
-> `0x00493cf0` `route_entry_collection_create_endpoint_entry_from_coords_and_policy`
-> `0x004952f0` `math_compute_quadrant_adjusted_heading_angle_from_xy_pair`
@ -3056,7 +3056,7 @@
-> `0x0040ab50` `simulation_advance_to_target_calendar_point`
-> `0x0051d3c0` `calendar_point_pack_tuple_to_absolute_counter`
- `0x0051db80` `math_measure_float_xy_pair_distance`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
-> `0x0049d380` `route_entry_collection_search_path_between_entry_or_coord_endpoints`
-> `0x00552900` `shell_queue_projected_world_anchor_quad`

View file

@ -7,7 +7,7 @@ digraph shell_load {
label="map";
color="#cccccc";
"0x004010f0" [label="0x004010f0\\ncity_compute_connection_bonus_candidate_weight", fillcolor="#f8f8f8"];
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_with_optional_direct_site_placement", fillcolor="#f8f8f8"];
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_and_optionally_place_direct_site", fillcolor="#f8f8f8"];
"0x00404640" [label="0x00404640\\ncity_connection_bonus_try_compact_route_builder_from_region_entry", fillcolor="#f8f8f8"];
"0x004046a0" [label="0x004046a0\\ncity_connection_bonus_build_peer_route_candidate", fillcolor="#f8f8f8"];
"0x00404c60" [label="0x00404c60\\ncity_connection_try_build_route_between_region_entry_pair", fillcolor="#f8f8f8"];
@ -247,7 +247,7 @@ digraph shell_load {
color="#cccccc";
"0x004014b0" [label="0x004014b0\\ncompany_try_buy_unowned_industry_near_city_and_publish_news", fillcolor="#f8f8f8"];
"0x00401860" [label="0x00401860\\ncompany_query_cached_linked_transit_route_anchor_entry_id", fillcolor="#f8f8f8"];
"0x004019e0" [label="0x004019e0\\ncompany_service_periodic_city_connection_finance_and_linked_transit_lanes", fillcolor="#f8f8f8"];
"0x004019e0" [label="0x004019e0\\ncompany_service_periodic_city_connection_finance_and_linked_transit", fillcolor="#f8f8f8"];
"0x00401c50" [label="0x00401c50\\ncompany_evaluate_annual_finance_policy_and_publish_news", fillcolor="#f8f8f8"];
"0x00404ce0" [label="0x00404ce0\\nsimulation_try_select_and_publish_company_start_or_city_connection_news", fillcolor="#f8f8f8"];
"0x00405920" [label="0x00405920\\ncompany_query_min_linked_site_distance_to_xy", fillcolor="#f8f8f8"];

View file

@ -12,9 +12,9 @@
| `0x004010f0` | `city_compute_connection_bonus_candidate_weight` | `map` | `4` |
| `0x004014b0` | `company_try_buy_unowned_industry_near_city_and_publish_news` | `simulation` | `2` |
| `0x00401860` | `company_query_cached_linked_transit_route_anchor_entry_id` | `simulation` | `3` |
| `0x004019e0` | `company_service_periodic_city_connection_finance_and_linked_transit_lanes` | `simulation` | `2` |
| `0x004019e0` | `company_service_periodic_city_connection_finance_and_linked_transit` | `simulation` | `2` |
| `0x00401c50` | `company_evaluate_annual_finance_policy_and_publish_news` | `simulation` | `2` |
| `0x00402cb0` | `city_connection_try_build_route_with_optional_direct_site_placement` | `map` | `3` |
| `0x00402cb0` | `city_connection_try_build_route_and_optionally_place_direct_site` | `map` | `3` |
| `0x00404640` | `city_connection_bonus_try_compact_route_builder_from_region_entry` | `map` | `3` |
| `0x004046a0` | `city_connection_bonus_build_peer_route_candidate` | `map` | `4` |
| `0x00404c60` | `city_connection_try_build_route_between_region_entry_pair` | `map` | `3` |
@ -309,12 +309,12 @@
-> `0x00406050` `company_evaluate_and_publish_city_connection_bonus_news`
-> `0x00424010` `company_has_territory_access_rights`
- `0x004014b0` `company_try_buy_unowned_industry_near_city_and_publish_news`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00426590` `company_count_linked_transit_sites`
-> `0x0051c920` `localization_lookup_display_label_by_stem_or_fallback`
- `0x00401860` `company_query_cached_linked_transit_route_anchor_entry_id`
-> `0x004801a0` `placed_structure_is_linked_transit_site_reachable_from_company_route_anchor`
- `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
- `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x004014b0` `company_try_buy_unowned_industry_near_city_and_publish_news`
-> `0x00401c50` `company_evaluate_annual_finance_policy_and_publish_news`
-> `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
@ -328,7 +328,7 @@
-> `0x0041d550` `locomotive_era_and_engine_type_pass_company_policy_gate`
-> `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
- `0x00401c50` `company_evaluate_annual_finance_policy_and_publish_news`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00423d70` `company_repay_bond_slot_and_compact_debt_table`
-> `0x00424fd0` `company_compute_public_support_adjusted_share_price_scalar`
-> `0x00425a90` `company_declare_bankruptcy_and_halve_bond_debt`
@ -339,7 +339,7 @@
-> `0x004275c0` `company_issue_bond_and_record_terms`
-> `0x0042a0e0` `company_query_highest_coupon_bond_slot_index`
-> `0x0042a5d0` `company_read_year_or_control_transfer_metric_value`
- `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
- `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
-> `0x00404c60` `city_connection_try_build_route_between_region_entry_pair`
@ -351,15 +351,15 @@
-> `0x00482e00` `runtime_query_hundredths_scaled_build_version`
-> `0x004a01a0` `route_entry_collection_try_build_path_between_optional_endpoint_entries`
- `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
- `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x00406050` `company_evaluate_and_publish_city_connection_bonus_news`
-> `0x00420280` `city_connection_bonus_select_first_matching_peer_site`
- `0x00404c60` `city_connection_try_build_route_between_region_entry_pair`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
- `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
-> `0x004010f0` `city_compute_connection_bonus_candidate_weight`
@ -381,7 +381,7 @@
-> `0x00427590` `company_connection_bonus_lane_is_unlocked`
-> `0x0042a5d0` `company_read_year_or_control_transfer_metric_value`
- `0x004078a0` `company_select_preferred_available_locomotive_id`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00409300` `company_publish_train_upgrade_news`
-> `0x00409830` `company_try_add_linked_transit_train_and_publish_news`
-> `0x0041d550` `locomotive_era_and_engine_type_pass_company_policy_gate`
@ -496,7 +496,7 @@
- `0x00417790` `map_angle_rotate_grid_offset_pair_into_world_offset_pair`
-> `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
- `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00417790` `map_angle_rotate_grid_offset_pair_into_world_offset_pair`
-> `0x004197e0` `placed_structure_validate_projected_candidate_placement`
- `0x00418040` `placed_structure_render_local_runtime_overlay_payload_from_projected_bounds`
@ -522,7 +522,7 @@
-> `0x00418040` `placed_structure_render_local_runtime_overlay_payload_from_projected_bounds`
-> `0x00418610` `world_grid_refresh_projected_rect_sample_band_and_flag_mask`
- `0x004197e0` `placed_structure_validate_projected_candidate_placement`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
-> `0x00424010` `company_has_territory_access_rights`
-> `0x004240a0` `company_query_available_track_laying_capacity_or_unlimited`
@ -843,7 +843,7 @@
- `0x00482d80` `runtime_query_cached_local_exe_version_string`
-> `0x00482d10` `runtime_query_cached_local_exe_version_float`
- `0x00482e00` `runtime_query_hundredths_scaled_build_version`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00482d10` `runtime_query_cached_local_exe_version_float`
-> `0x004a65b0` `aux_route_entry_tracker_dispatch_route_entry_pair_metric_query`
- `0x004839b0` `shell_setup_query_file_list_uses_map_extension_pattern`
@ -875,8 +875,8 @@
- `0x004955b0` `route_entry_collection_map_track_lay_mode_to_endpoint_policy_byte`
-> `0x00493cf0` `route_entry_collection_create_endpoint_entry_from_coords_and_policy`
- `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00494cb0` `route_entry_collection_try_find_route_entry_covering_point_window`
-> `0x0049d380` `route_entry_collection_search_path_between_entry_or_coord_endpoints`
-> `0x004a6630` `aux_route_entry_tracker_query_best_route_entry_pair_metric_with_endpoint_fallbacks`
@ -898,7 +898,7 @@
-> `0x0051db80` `math_measure_float_xy_pair_distance`
-> `0x005a152e` `math_abs_double_with_crt_special_case_handling`
- `0x004a01a0` `route_entry_collection_try_build_path_between_optional_endpoint_entries`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00480bb0` `placed_structure_refresh_linked_site_display_name_and_route_anchor`
-> `0x00493cf0` `route_entry_collection_create_endpoint_entry_from_coords_and_policy`
-> `0x00494f00` `aux_route_entry_tracker_merge_or_bind_endpoint_entry`
@ -1310,7 +1310,7 @@
- `0x0051c920` `localization_lookup_display_label_by_stem_or_fallback`
-> `0x004ba3d0` `shell_building_detail_refresh_subject_cargo_and_service_rows`
- `0x0051db80` `math_measure_float_xy_pair_distance`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
-> `0x0049d380` `route_entry_collection_search_path_between_entry_or_coord_endpoints`
-> `0x00552900` `shell_queue_projected_world_anchor_quad`

View file

@ -16,7 +16,7 @@ digraph shell_load {
color="#cccccc";
"0x004010f0" [label="0x004010f0\\ncity_compute_connection_bonus_candidate_weight", fillcolor="#f8f8f8"];
"0x00402c90" [label="0x00402c90\\nplaced_structure_resolve_linked_candidate_record", fillcolor="#f8f8f8"];
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_with_optional_direct_site_placement", fillcolor="#f8f8f8"];
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_and_optionally_place_direct_site", fillcolor="#f8f8f8"];
"0x00404640" [label="0x00404640\\ncity_connection_bonus_try_compact_route_builder_from_region_entry", fillcolor="#f8f8f8"];
"0x004046a0" [label="0x004046a0\\ncity_connection_bonus_build_peer_route_candidate", fillcolor="#f8f8f8"];
"0x00404c60" [label="0x00404c60\\ncity_connection_try_build_route_between_region_entry_pair", fillcolor="#f8f8f8"];
@ -472,7 +472,7 @@ digraph shell_load {
"0x004014b0" [label="0x004014b0\\ncompany_try_buy_unowned_industry_near_city_and_publish_news", fillcolor="#f8f8f8"];
"0x00401860" [label="0x00401860\\ncompany_query_cached_linked_transit_route_anchor_entry_id", fillcolor="#f8f8f8"];
"0x00401940" [label="0x00401940\\ncompany_reset_linked_transit_caches_and_reseed_empty_train_routes", fillcolor="#f8f8f8"];
"0x004019e0" [label="0x004019e0\\ncompany_service_periodic_city_connection_finance_and_linked_transit_lanes", fillcolor="#f8f8f8"];
"0x004019e0" [label="0x004019e0\\ncompany_service_periodic_city_connection_finance_and_linked_transit", fillcolor="#f8f8f8"];
"0x00401c50" [label="0x00401c50\\ncompany_evaluate_annual_finance_policy_and_publish_news", fillcolor="#f8f8f8"];
"0x00404ce0" [label="0x00404ce0\\nsimulation_try_select_and_publish_company_start_or_city_connection_news", fillcolor="#f8f8f8"];
"0x00405920" [label="0x00405920\\ncompany_query_min_linked_site_distance_to_xy", fillcolor="#f8f8f8"];

View file

@ -13,10 +13,10 @@
| `0x004014b0` | `company_try_buy_unowned_industry_near_city_and_publish_news` | `simulation` | `2` |
| `0x00401860` | `company_query_cached_linked_transit_route_anchor_entry_id` | `simulation` | `3` |
| `0x00401940` | `company_reset_linked_transit_caches_and_reseed_empty_train_routes` | `simulation` | `2` |
| `0x004019e0` | `company_service_periodic_city_connection_finance_and_linked_transit_lanes` | `simulation` | `2` |
| `0x004019e0` | `company_service_periodic_city_connection_finance_and_linked_transit` | `simulation` | `2` |
| `0x00401c50` | `company_evaluate_annual_finance_policy_and_publish_news` | `simulation` | `2` |
| `0x00402c90` | `placed_structure_resolve_linked_candidate_record` | `map` | `2` |
| `0x00402cb0` | `city_connection_try_build_route_with_optional_direct_site_placement` | `map` | `3` |
| `0x00402cb0` | `city_connection_try_build_route_and_optionally_place_direct_site` | `map` | `3` |
| `0x00404640` | `city_connection_bonus_try_compact_route_builder_from_region_entry` | `map` | `3` |
| `0x004046a0` | `city_connection_bonus_build_peer_route_candidate` | `map` | `4` |
| `0x00404c60` | `city_connection_try_build_route_between_region_entry_pair` | `map` | `3` |
@ -541,7 +541,7 @@
-> `0x00406050` `company_evaluate_and_publish_city_connection_bonus_news`
-> `0x00424010` `company_has_territory_access_rights`
- `0x004014b0` `company_try_buy_unowned_industry_near_city_and_publish_news`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00426590` `company_count_linked_transit_sites`
-> `0x0051c920` `localization_lookup_display_label_by_stem_or_fallback`
- `0x00401860` `company_query_cached_linked_transit_route_anchor_entry_id`
@ -550,7 +550,7 @@
-> `0x00409720` `company_service_linked_transit_site_caches`
-> `0x00409770` `train_try_append_linked_transit_autoroute_entry`
-> `0x004b3000` `train_route_list_remove_entry_and_compact`
- `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
- `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x004014b0` `company_try_buy_unowned_industry_near_city_and_publish_news`
-> `0x00401c50` `company_evaluate_annual_finance_policy_and_publish_news`
-> `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
@ -564,7 +564,7 @@
-> `0x0041d550` `locomotive_era_and_engine_type_pass_company_policy_gate`
-> `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
- `0x00401c50` `company_evaluate_annual_finance_policy_and_publish_news`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00423d70` `company_repay_bond_slot_and_compact_debt_table`
-> `0x00424fd0` `company_compute_public_support_adjusted_share_price_scalar`
-> `0x00425a90` `company_declare_bankruptcy_and_halve_bond_debt`
@ -577,7 +577,7 @@
-> `0x0042a5d0` `company_read_year_or_control_transfer_metric_value`
- `0x00402c90` `placed_structure_resolve_linked_candidate_record`
-> `0x00518140` `indexed_collection_resolve_live_entry_by_id`
- `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
- `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
-> `0x00404c60` `city_connection_try_build_route_between_region_entry_pair`
@ -589,15 +589,15 @@
-> `0x00482e00` `runtime_query_hundredths_scaled_build_version`
-> `0x004a01a0` `route_entry_collection_try_build_path_between_optional_endpoint_entries`
- `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
- `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x00406050` `company_evaluate_and_publish_city_connection_bonus_news`
-> `0x00420280` `city_connection_bonus_select_first_matching_peer_site`
- `0x00404c60` `city_connection_try_build_route_between_region_entry_pair`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
- `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
-> `0x004010f0` `city_compute_connection_bonus_candidate_weight`
@ -619,7 +619,7 @@
-> `0x00427590` `company_connection_bonus_lane_is_unlocked`
-> `0x0042a5d0` `company_read_year_or_control_transfer_metric_value`
- `0x004078a0` `company_select_preferred_available_locomotive_id`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00409300` `company_publish_train_upgrade_news`
-> `0x00409830` `company_try_add_linked_transit_train_and_publish_news`
-> `0x0041d550` `locomotive_era_and_engine_type_pass_company_policy_gate`
@ -763,7 +763,7 @@
- `0x00417790` `map_angle_rotate_grid_offset_pair_into_world_offset_pair`
-> `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
- `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00417790` `map_angle_rotate_grid_offset_pair_into_world_offset_pair`
-> `0x004197e0` `placed_structure_validate_projected_candidate_placement`
- `0x00418040` `placed_structure_render_local_runtime_overlay_payload_from_projected_bounds`
@ -790,7 +790,7 @@
-> `0x00418040` `placed_structure_render_local_runtime_overlay_payload_from_projected_bounds`
-> `0x00418610` `world_grid_refresh_projected_rect_sample_band_and_flag_mask`
- `0x004197e0` `placed_structure_validate_projected_candidate_placement`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
-> `0x00424010` `company_has_territory_access_rights`
-> `0x004240a0` `company_query_available_track_laying_capacity_or_unlimited`
@ -1254,7 +1254,7 @@
-> `0x0046a6c0` `multiplayer_session_event_publish_registration_field`
-> `0x00482d10` `runtime_query_cached_local_exe_version_float`
- `0x00482e00` `runtime_query_hundredths_scaled_build_version`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00482d10` `runtime_query_cached_local_exe_version_float`
-> `0x004a65b0` `aux_route_entry_tracker_dispatch_route_entry_pair_metric_query`
- `0x00482ec0` `shell_transition_mode`
@ -1349,8 +1349,8 @@
- `0x004955b0` `route_entry_collection_map_track_lay_mode_to_endpoint_policy_byte`
-> `0x00493cf0` `route_entry_collection_create_endpoint_entry_from_coords_and_policy`
- `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00494cb0` `route_entry_collection_try_find_route_entry_covering_point_window`
-> `0x0049d380` `route_entry_collection_search_path_between_entry_or_coord_endpoints`
-> `0x004a6630` `aux_route_entry_tracker_query_best_route_entry_pair_metric_with_endpoint_fallbacks`
@ -1372,7 +1372,7 @@
-> `0x0051db80` `math_measure_float_xy_pair_distance`
-> `0x005a152e` `math_abs_double_with_crt_special_case_handling`
- `0x004a01a0` `route_entry_collection_try_build_path_between_optional_endpoint_entries`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00480bb0` `placed_structure_refresh_linked_site_display_name_and_route_anchor`
-> `0x00493cf0` `route_entry_collection_create_endpoint_entry_from_coords_and_policy`
-> `0x00494f00` `aux_route_entry_tracker_merge_or_bind_endpoint_entry`
@ -2193,7 +2193,7 @@
- `0x0051c920` `localization_lookup_display_label_by_stem_or_fallback`
-> `0x004ba3d0` `shell_building_detail_refresh_subject_cargo_and_service_rows`
- `0x0051db80` `math_measure_float_xy_pair_distance`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
-> `0x0049d380` `route_entry_collection_search_path_between_entry_or_coord_endpoints`
-> `0x00552900` `shell_queue_projected_world_anchor_quad`

View file

@ -12,7 +12,7 @@ digraph shell_load {
subgraph cluster_map {
label="map";
color="#cccccc";
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_with_optional_direct_site_placement", fillcolor="#f8f8f8"];
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_and_optionally_place_direct_site", fillcolor="#f8f8f8"];
"0x00404640" [label="0x00404640\\ncity_connection_bonus_try_compact_route_builder_from_region_entry", fillcolor="#f8f8f8"];
"0x004046a0" [label="0x004046a0\\ncity_connection_bonus_build_peer_route_candidate", fillcolor="#f8f8f8"];
"0x00404c60" [label="0x00404c60\\ncity_connection_try_build_route_between_region_entry_pair", fillcolor="#f8f8f8"];

View file

@ -9,7 +9,7 @@
| Address | Name | Subsystem | Confidence |
| --- | --- | --- | --- |
| `0x00402cb0` | `city_connection_try_build_route_with_optional_direct_site_placement` | `map` | `3` |
| `0x00402cb0` | `city_connection_try_build_route_and_optionally_place_direct_site` | `map` | `3` |
| `0x00404640` | `city_connection_bonus_try_compact_route_builder_from_region_entry` | `map` | `3` |
| `0x004046a0` | `city_connection_bonus_build_peer_route_candidate` | `map` | `4` |
| `0x00404c60` | `city_connection_try_build_route_between_region_entry_pair` | `map` | `3` |
@ -138,7 +138,7 @@
## Edges
- `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
- `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
-> `0x00404c60` `city_connection_try_build_route_between_region_entry_pair`
@ -150,13 +150,13 @@
-> `0x00482e00` `runtime_query_hundredths_scaled_build_version`
-> `0x004a01a0` `route_entry_collection_try_build_path_between_optional_endpoint_entries`
- `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
- `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
- `0x00404c60` `city_connection_try_build_route_between_region_entry_pair`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
- `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
-> `0x00404c60` `city_connection_try_build_route_between_region_entry_pair`
@ -195,12 +195,12 @@
-> `0x00518140` `indexed_collection_resolve_live_entry_by_id`
-> `0x00518380` `indexed_collection_get_nth_live_entry_id`
- `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x004197e0` `placed_structure_validate_projected_candidate_placement`
- `0x00418a60` `placed_structure_clone_template_local_runtime_record_for_subject_and_refresh_component_bounds`
-> `0x0040e450` `placed_structure_refresh_cloned_local_runtime_record_from_current_candidate_stem`
- `0x004197e0` `placed_structure_validate_projected_candidate_placement`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
- `0x0041e2b0` `structure_candidate_rebuild_local_service_metrics`
-> `0x0041e220` `structure_candidate_is_enabled_for_current_year`
@ -284,7 +284,7 @@
-> `0x0046a6c0` `multiplayer_session_event_publish_registration_field`
-> `0x00482d10` `runtime_query_cached_local_exe_version_float`
- `0x00482e00` `runtime_query_hundredths_scaled_build_version`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00482d10` `runtime_query_cached_local_exe_version_float`
-> `0x004a65b0` `aux_route_entry_tracker_dispatch_route_entry_pair_metric_query`
- `0x00482ec0` `shell_transition_mode`
@ -314,7 +314,7 @@
- `0x004882e0` `world_region_border_overlay_rebuild`
-> `0x004384d0` `world_run_post_load_generation_pipeline`
- `0x004a01a0` `route_entry_collection_try_build_path_between_optional_endpoint_entries`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00518140` `indexed_collection_resolve_live_entry_by_id`
- `0x004a5280` `aux_route_entry_tracker_query_route_entry_pair_metric_via_weighted_recursive_search`
-> `0x004a65b0` `aux_route_entry_tracker_dispatch_route_entry_pair_metric_query`

View file

@ -17,7 +17,7 @@ digraph shell_load {
color="#cccccc";
"0x004010f0" [label="0x004010f0\\ncity_compute_connection_bonus_candidate_weight", fillcolor="#f8f8f8"];
"0x00402c90" [label="0x00402c90\\nplaced_structure_resolve_linked_candidate_record", fillcolor="#f8f8f8"];
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_with_optional_direct_site_placement", fillcolor="#f8f8f8"];
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_and_optionally_place_direct_site", fillcolor="#f8f8f8"];
"0x00404640" [label="0x00404640\\ncity_connection_bonus_try_compact_route_builder_from_region_entry", fillcolor="#f8f8f8"];
"0x004046a0" [label="0x004046a0\\ncity_connection_bonus_build_peer_route_candidate", fillcolor="#f8f8f8"];
"0x00404c60" [label="0x00404c60\\ncity_connection_try_build_route_between_region_entry_pair", fillcolor="#f8f8f8"];
@ -352,7 +352,7 @@ digraph shell_load {
label="simulation";
color="#cccccc";
"0x004014b0" [label="0x004014b0\\ncompany_try_buy_unowned_industry_near_city_and_publish_news", fillcolor="#f8f8f8"];
"0x004019e0" [label="0x004019e0\\ncompany_service_periodic_city_connection_finance_and_linked_transit_lanes", fillcolor="#f8f8f8"];
"0x004019e0" [label="0x004019e0\\ncompany_service_periodic_city_connection_finance_and_linked_transit", fillcolor="#f8f8f8"];
"0x00401c50" [label="0x00401c50\\ncompany_evaluate_annual_finance_policy_and_publish_news", fillcolor="#f8f8f8"];
"0x00404ce0" [label="0x00404ce0\\nsimulation_try_select_and_publish_company_start_or_city_connection_news", fillcolor="#f8f8f8"];
"0x00405920" [label="0x00405920\\ncompany_query_min_linked_site_distance_to_xy", fillcolor="#f8f8f8"];

View file

@ -11,10 +11,10 @@
| --- | --- | --- | --- |
| `0x004010f0` | `city_compute_connection_bonus_candidate_weight` | `map` | `4` |
| `0x004014b0` | `company_try_buy_unowned_industry_near_city_and_publish_news` | `simulation` | `2` |
| `0x004019e0` | `company_service_periodic_city_connection_finance_and_linked_transit_lanes` | `simulation` | `2` |
| `0x004019e0` | `company_service_periodic_city_connection_finance_and_linked_transit` | `simulation` | `2` |
| `0x00401c50` | `company_evaluate_annual_finance_policy_and_publish_news` | `simulation` | `2` |
| `0x00402c90` | `placed_structure_resolve_linked_candidate_record` | `map` | `2` |
| `0x00402cb0` | `city_connection_try_build_route_with_optional_direct_site_placement` | `map` | `3` |
| `0x00402cb0` | `city_connection_try_build_route_and_optionally_place_direct_site` | `map` | `3` |
| `0x00404640` | `city_connection_bonus_try_compact_route_builder_from_region_entry` | `map` | `3` |
| `0x004046a0` | `city_connection_bonus_build_peer_route_candidate` | `map` | `4` |
| `0x00404c60` | `city_connection_try_build_route_between_region_entry_pair` | `map` | `3` |
@ -385,10 +385,10 @@
-> `0x00406050` `company_evaluate_and_publish_city_connection_bonus_news`
-> `0x00424010` `company_has_territory_access_rights`
- `0x004014b0` `company_try_buy_unowned_industry_near_city_and_publish_news`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00426590` `company_count_linked_transit_sites`
-> `0x0051c920` `localization_lookup_display_label_by_stem_or_fallback`
- `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
- `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x004014b0` `company_try_buy_unowned_industry_near_city_and_publish_news`
-> `0x00401c50` `company_evaluate_annual_finance_policy_and_publish_news`
-> `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
@ -397,13 +397,13 @@
-> `0x004093d0` `company_rebuild_linked_transit_site_peer_cache`
-> `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
- `0x00401c50` `company_evaluate_annual_finance_policy_and_publish_news`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00423d70` `company_repay_bond_slot_and_compact_debt_table`
-> `0x00426260` `company_compute_board_approved_dividend_rate_ceiling`
-> `0x0042a5d0` `company_read_year_or_control_transfer_metric_value`
- `0x00402c90` `placed_structure_resolve_linked_candidate_record`
-> `0x00518140` `indexed_collection_resolve_live_entry_by_id`
- `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
- `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
-> `0x00404c60` `city_connection_try_build_route_between_region_entry_pair`
@ -415,15 +415,15 @@
-> `0x00482e00` `runtime_query_hundredths_scaled_build_version`
-> `0x004a01a0` `route_entry_collection_try_build_path_between_optional_endpoint_entries`
- `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
- `0x004046a0` `city_connection_bonus_build_peer_route_candidate`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404640` `city_connection_bonus_try_compact_route_builder_from_region_entry`
-> `0x00406050` `company_evaluate_and_publish_city_connection_bonus_news`
-> `0x00420280` `city_connection_bonus_select_first_matching_peer_site`
- `0x00404c60` `city_connection_try_build_route_between_region_entry_pair`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
- `0x00404ce0` `simulation_try_select_and_publish_company_start_or_city_connection_news`
-> `0x004010f0` `city_compute_connection_bonus_candidate_weight`
@ -507,12 +507,12 @@
-> `0x00518140` `indexed_collection_resolve_live_entry_by_id`
-> `0x00518380` `indexed_collection_get_nth_live_entry_id`
- `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x004197e0` `placed_structure_validate_projected_candidate_placement`
- `0x00418a60` `placed_structure_clone_template_local_runtime_record_for_subject_and_refresh_component_bounds`
-> `0x0040e450` `placed_structure_refresh_cloned_local_runtime_record_from_current_candidate_stem`
- `0x004197e0` `placed_structure_validate_projected_candidate_placement`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
-> `0x00424010` `company_has_territory_access_rights`
- `0x0041e2b0` `structure_candidate_rebuild_local_service_metrics`
@ -754,7 +754,7 @@
-> `0x0046a6c0` `multiplayer_session_event_publish_registration_field`
-> `0x00482d10` `runtime_query_cached_local_exe_version_float`
- `0x00482e00` `runtime_query_hundredths_scaled_build_version`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00482d10` `runtime_query_cached_local_exe_version_float`
-> `0x004a65b0` `aux_route_entry_tracker_dispatch_route_entry_pair_metric_query`
- `0x00482ec0` `shell_transition_mode`
@ -809,12 +809,12 @@
- `0x00494240` `route_entry_collection_query_rect_window_passes_entry_type_gate`
-> `0x00518140` `indexed_collection_resolve_live_entry_by_id`
- `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit_lanes`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x004019e0` `company_service_periodic_city_connection_finance_and_linked_transit`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x004a6630` `aux_route_entry_tracker_query_best_route_entry_pair_metric_with_endpoint_fallbacks`
-> `0x0051db80` `math_measure_float_xy_pair_distance`
- `0x004a01a0` `route_entry_collection_try_build_path_between_optional_endpoint_entries`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00518140` `indexed_collection_resolve_live_entry_by_id`
- `0x004a5280` `aux_route_entry_tracker_query_route_entry_pair_metric_via_weighted_recursive_search`
-> `0x004a65b0` `aux_route_entry_tracker_dispatch_route_entry_pair_metric_query`
@ -1408,7 +1408,7 @@
- `0x0051c920` `localization_lookup_display_label_by_stem_or_fallback`
-> `0x004ba3d0` `shell_building_detail_refresh_subject_cargo_and_service_rows`
- `0x0051db80` `math_measure_float_xy_pair_distance`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
- `0x0051ef20` `shell_load_display_runtime_config_or_init_defaults`
-> `0x0051ebc0` `shell_reset_display_runtime_defaults`

View file

@ -18,7 +18,7 @@ digraph shell_load {
subgraph cluster_map {
label="map";
color="#cccccc";
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_with_optional_direct_site_placement", fillcolor="#f8f8f8"];
"0x00402cb0" [label="0x00402cb0\\ncity_connection_try_build_route_and_optionally_place_direct_site", fillcolor="#f8f8f8"];
"0x0040b5d0" [label="0x0040b5d0\\nsupport_collection_refresh_records_from_tagged_bundle", fillcolor="#f8f8f8"];
"0x0040b6a0" [label="0x0040b6a0\\nsupport_collection_serialize_tagged_records_into_bundle", fillcolor="#f8f8f8"];
"0x0040b720" [label="0x0040b720\\nsupport_collection_release_entries_and_collection_storage", fillcolor="#f8f8f8"];

View file

@ -9,7 +9,7 @@
| Address | Name | Subsystem | Confidence |
| --- | --- | --- | --- |
| `0x00402cb0` | `city_connection_try_build_route_with_optional_direct_site_placement` | `map` | `3` |
| `0x00402cb0` | `city_connection_try_build_route_and_optionally_place_direct_site` | `map` | `3` |
| `0x004078a0` | `company_select_preferred_available_locomotive_id` | `simulation` | `3` |
| `0x00409e80` | `world_set_selected_year_and_refresh_calendar_presentation_state` | `simulation` | `3` |
| `0x0040a590` | `simulation_service_periodic_boundary_work` | `simulation` | `3` |
@ -355,7 +355,7 @@
## Edges
- `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
- `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
-> `0x004197e0` `placed_structure_validate_projected_candidate_placement`
-> `0x004423a0` `shell_status_stack_push_and_service_active_tracklay_and_stationplace_tools`
@ -502,7 +502,7 @@
-> `0x00448af0` `world_query_compact_grid_flag_bitset_membership_by_mode`
-> `0x00534e10` `world_secondary_raster_query_cell_class_in_set_1_3_4_5`
- `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x004197e0` `placed_structure_validate_projected_candidate_placement`
- `0x00418a60` `placed_structure_clone_template_local_runtime_record_for_subject_and_refresh_component_bounds`
-> `0x0040e450` `placed_structure_refresh_cloned_local_runtime_record_from_current_candidate_stem`
@ -510,7 +510,7 @@
-> `0x00416e20` `indexed_collection_resolve_live_entry_id_by_stem_string`
-> `0x00416ec0` `placed_structure_build_projected_runtime_scratch_from_candidate_and_coords`
- `0x004197e0` `placed_structure_validate_projected_candidate_placement`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00413df0` `projected_rect_packed_cell_list_try_append_unique_xy_with_optional_highbit_flag_and_expand_quarter_bounds`
-> `0x00414bd0` `world_query_float_coords_within_live_grid_bounds`
-> `0x00417840` `placed_structure_project_candidate_grid_extent_offset_by_rotation`
@ -890,7 +890,7 @@
- `0x00441f70` `shell_map_bundle_load_companion_image_file_into_global_staging_buffer_and_sync_tags`
-> `0x00441ec0` `shell_map_bundle_serialize_or_restore_stage_dword_and_fixed_preview_payload_tags_0x2ee0_0x2ee1_0x3c2`
- `0x004423a0` `shell_status_stack_push_and_service_active_tracklay_and_stationplace_tools`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00446240` `world_runtime_serialize_smp_bundle`
- `0x004423d0` `shell_status_stack_pop_restore_and_service_active_stationplace_and_tracklay_tools`
-> `0x004423a0` `shell_status_stack_push_and_service_active_tracklay_and_stationplace_tools`
@ -1231,7 +1231,7 @@
- `0x00482d10` `runtime_query_cached_local_exe_version_float`
-> `0x00482e00` `runtime_query_hundredths_scaled_build_version`
- `0x00482e00` `runtime_query_hundredths_scaled_build_version`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00482d10` `runtime_query_cached_local_exe_version_float`
-> `0x004a65b0` `aux_route_entry_tracker_dispatch_route_entry_pair_metric_query`
-> `0x005a10d0` `math_round_st0_to_signed_qword_with_current_x87_mode`
@ -1321,7 +1321,7 @@
- `0x00495020` `aux_route_entry_tracker_refresh_cached_match_fields_and_maybe_split_duplicate_pair`
-> `0x004a41b0` `route_entry_tracker_collection_refresh_records_from_tagged_bundle`
- `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x00494cb0` `route_entry_collection_try_find_route_entry_covering_point_window`
-> `0x0049d380` `route_entry_collection_search_path_between_entry_or_coord_endpoints`
-> `0x0051db80` `math_measure_float_xy_pair_distance`
@ -1498,7 +1498,7 @@
-> `0x0040ab50` `simulation_advance_to_target_calendar_point`
-> `0x0051d3c0` `calendar_point_pack_tuple_to_absolute_counter`
- `0x0051db80` `math_measure_float_xy_pair_distance`
-> `0x00402cb0` `city_connection_try_build_route_with_optional_direct_site_placement`
-> `0x00402cb0` `city_connection_try_build_route_and_optionally_place_direct_site`
-> `0x0049bd40` `route_entry_collection_run_initial_candidate_path_sweep`
-> `0x0049d380` `route_entry_collection_search_path_between_entry_or_coord_endpoints`
-> `0x00552900` `shell_queue_projected_world_anchor_quad`

View file

@ -0,0 +1,20 @@
use super::{Command, FinanceCommand, usage_error};
pub(super) fn parse_finance_command(
args: &[String],
) -> Result<Command, Box<dyn std::error::Error>> {
match args {
[subcommand, snapshot_path] if subcommand == "eval" => {
Ok(Command::Finance(FinanceCommand::Eval {
snapshot_path: snapshot_path.into(),
}))
}
[subcommand, left_path, right_path] if subcommand == "diff" => {
Ok(Command::Finance(FinanceCommand::Diff {
left_path: left_path.into(),
right_path: right_path.into(),
}))
}
_ => usage_error(),
}
}

View file

@ -0,0 +1,158 @@
mod finance;
mod model;
mod runtime;
mod validate;
use std::env;
use std::path::Path;
pub(crate) use model::{
Command, CompareCommand, FinanceCommand, FixtureStateCommand, InspectCommand, RuntimeCommand,
ScanCommand,
};
const USAGE: &str = "usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime snapshot-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime inspect-candidate-table <file.smp> | runtime inspect-compact-event-dispatch-cluster <maps-dir> | runtime inspect-compact-event-dispatch-cluster-counts <maps-dir> | runtime inspect-map-title-hints <maps-dir> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime inspect-save-company-chairman <file.smp> | runtime inspect-save-placed-structure-triplets <file.smp> | runtime compare-region-fixed-row-runs <left.gms> <right.gms> | runtime inspect-periodic-company-service-trace <file.smp> | runtime inspect-region-service-trace <file.smp> | runtime inspect-infrastructure-asset-trace <file.smp> | runtime inspect-save-region-queued-notice-records <file.smp> | runtime inspect-placed-structure-dynamic-side-buffer <file.smp> | runtime inspect-unclassified-save-collections <file.smp> | runtime snapshot-save-state <file.smp> <snapshot.json> | runtime export-save-slice <file.smp> <save-slice.json> | runtime export-overlay-import <snapshot.json> <save-slice.json> <overlay-import.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-cargo-types <CargoTypes-dir> | runtime inspect-building-type-sources <BuildingTypes-dir> [building-bindings.json] | runtime inspect-cargo-skins <Cargo106.PK4> | runtime inspect-cargo-economy-sources <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-production-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-cargo-price-selector <CargoTypes-dir> <Cargo106.PK4> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-candidate-table-named-runs <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]";
pub(super) fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().skip(1).collect();
let current_dir = env::current_dir()?;
parse_command_args(&args, &current_dir)
}
fn parse_command_args(
args: &[String],
current_dir: &Path,
) -> Result<Command, Box<dyn std::error::Error>> {
match args {
[] => Ok(Command::Validate {
repo_root: current_dir.to_path_buf(),
}),
[command, rest @ ..] if command == "validate" => {
validate::parse_validate_command(rest, current_dir)
}
[command, rest @ ..] if command == "finance" => finance::parse_finance_command(rest),
[command, rest @ ..] if command == "runtime" => runtime::parse_runtime_command(rest),
_ => usage_error(),
}
}
fn usage_error<T>() -> Result<T, Box<dyn std::error::Error>> {
Err(USAGE.into())
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{
Command, CompareCommand, FinanceCommand, FixtureStateCommand, InspectCommand,
RuntimeCommand, ScanCommand, parse_command_args,
};
fn parse(args: &[&str]) -> Command {
parse_command_args(
&args
.iter()
.map(|arg| (*arg).to_string())
.collect::<Vec<_>>(),
PathBuf::from("/tmp/workspace").as_path(),
)
.expect("command should parse")
}
#[test]
fn parses_validate_with_default_repo_root() {
assert_eq!(
parse(&[]),
Command::Validate {
repo_root: PathBuf::from("/tmp/workspace"),
}
);
}
#[test]
fn parses_finance_eval() {
assert_eq!(
parse(&["finance", "eval", "snapshot.json"]),
Command::Finance(FinanceCommand::Eval {
snapshot_path: PathBuf::from("snapshot.json"),
})
);
}
#[test]
fn parses_finance_diff() {
assert_eq!(
parse(&["finance", "diff", "left.json", "right.json"]),
Command::Finance(FinanceCommand::Diff {
left_path: PathBuf::from("left.json"),
right_path: PathBuf::from("right.json"),
})
);
}
#[test]
fn parses_runtime_snapshot_state_command() {
assert_eq!(
parse(&["runtime", "snapshot-state", "input.json", "snapshot.json"]),
Command::Runtime(RuntimeCommand::FixtureState(
FixtureStateCommand::SnapshotState {
input_path: PathBuf::from("input.json"),
output_path: PathBuf::from("snapshot.json"),
}
))
);
}
#[test]
fn parses_runtime_snapshot_save_state_command() {
assert_eq!(
parse(&[
"runtime",
"snapshot-save-state",
"save.gms",
"snapshot.json"
]),
Command::Runtime(RuntimeCommand::FixtureState(
FixtureStateCommand::SnapshotSaveState {
smp_path: PathBuf::from("save.gms"),
output_path: PathBuf::from("snapshot.json"),
}
))
);
}
#[test]
fn parses_runtime_inspect_command() {
assert_eq!(
parse(&["runtime", "inspect-campaign-exe", "RT3.exe"]),
Command::Runtime(RuntimeCommand::Inspect(
InspectCommand::InspectCampaignExe {
exe_path: PathBuf::from("RT3.exe"),
}
))
);
}
#[test]
fn parses_runtime_compare_command() {
assert_eq!(
parse(&["runtime", "compare-candidate-table", "a.gms", "b.gms"]),
Command::Runtime(RuntimeCommand::Compare(
CompareCommand::CompareCandidateTable {
smp_paths: vec![PathBuf::from("a.gms"), PathBuf::from("b.gms")],
}
))
);
}
#[test]
fn parses_runtime_scan_command() {
assert_eq!(
parse(&["runtime", "scan-special-conditions", "root"]),
Command::Runtime(RuntimeCommand::Scan(ScanCommand::ScanSpecialConditions {
root_path: PathBuf::from("root"),
}))
);
}
}

View file

@ -0,0 +1,194 @@
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Command {
Validate { repo_root: PathBuf },
Finance(FinanceCommand),
Runtime(RuntimeCommand),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FinanceCommand {
Eval {
snapshot_path: PathBuf,
},
Diff {
left_path: PathBuf,
right_path: PathBuf,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RuntimeCommand {
FixtureState(FixtureStateCommand),
Inspect(InspectCommand),
Compare(CompareCommand),
Scan(ScanCommand),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FixtureStateCommand {
ValidateFixture {
fixture_path: PathBuf,
},
SummarizeFixture {
fixture_path: PathBuf,
},
ExportFixtureState {
fixture_path: PathBuf,
output_path: PathBuf,
},
DiffState {
left_path: PathBuf,
right_path: PathBuf,
},
SummarizeState {
snapshot_path: PathBuf,
},
SnapshotState {
input_path: PathBuf,
output_path: PathBuf,
},
SummarizeSaveLoad {
smp_path: PathBuf,
},
LoadSaveSlice {
smp_path: PathBuf,
},
SnapshotSaveState {
smp_path: PathBuf,
output_path: PathBuf,
},
ExportSaveSlice {
smp_path: PathBuf,
output_path: PathBuf,
},
ExportOverlayImport {
snapshot_path: PathBuf,
save_slice_path: PathBuf,
output_path: PathBuf,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum InspectCommand {
InspectSmp {
smp_path: PathBuf,
},
InspectCandidateTable {
smp_path: PathBuf,
},
InspectCompactEventDispatchCluster {
root_path: PathBuf,
},
InspectCompactEventDispatchClusterCounts {
root_path: PathBuf,
},
InspectMapTitleHints {
root_path: PathBuf,
},
InspectSaveCompanyChairman {
smp_path: PathBuf,
},
InspectSavePlacedStructureTriplets {
smp_path: PathBuf,
},
InspectPeriodicCompanyServiceTrace {
smp_path: PathBuf,
},
InspectRegionServiceTrace {
smp_path: PathBuf,
},
InspectInfrastructureAssetTrace {
smp_path: PathBuf,
},
InspectSaveRegionQueuedNoticeRecords {
smp_path: PathBuf,
},
InspectPlacedStructureDynamicSideBuffer {
smp_path: PathBuf,
},
InspectUnclassifiedSaveCollections {
smp_path: PathBuf,
},
InspectPk4 {
pk4_path: PathBuf,
},
InspectCargoTypes {
cargo_types_dir: PathBuf,
},
InspectBuildingTypeSources {
building_types_dir: PathBuf,
bindings_path: Option<PathBuf>,
},
InspectCargoSkins {
cargo_skin_pk4_path: PathBuf,
},
InspectCargoEconomySources {
cargo_types_dir: PathBuf,
cargo_skin_pk4_path: PathBuf,
},
InspectCargoProductionSelector {
cargo_types_dir: PathBuf,
cargo_skin_pk4_path: PathBuf,
},
InspectCargoPriceSelector {
cargo_types_dir: PathBuf,
cargo_skin_pk4_path: PathBuf,
},
InspectWin {
win_path: PathBuf,
},
ExtractPk4Entry {
pk4_path: PathBuf,
entry_name: String,
output_path: PathBuf,
},
InspectCampaignExe {
exe_path: PathBuf,
},
ExportProfileBlock {
smp_path: PathBuf,
output_path: PathBuf,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CompareCommand {
CompareRegionFixedRowRuns {
left_path: PathBuf,
right_path: PathBuf,
},
CompareClassicProfile {
smp_paths: Vec<PathBuf>,
},
CompareRt3105Profile {
smp_paths: Vec<PathBuf>,
},
CompareCandidateTable {
smp_paths: Vec<PathBuf>,
},
CompareRecipeBookLines {
smp_paths: Vec<PathBuf>,
},
CompareSetupPayloadCore {
smp_paths: Vec<PathBuf>,
},
CompareSetupLaunchPayload {
smp_paths: Vec<PathBuf>,
},
ComparePostSpecialConditionsScalars {
smp_paths: Vec<PathBuf>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ScanCommand {
ScanCandidateTableHeaders { root_path: PathBuf },
ScanCandidateTableNamedRuns { root_path: PathBuf },
ScanSpecialConditions { root_path: PathBuf },
ScanAlignedRuntimeRuleBand { root_path: PathBuf },
ScanPostSpecialConditionsScalars { root_path: PathBuf },
ScanPostSpecialConditionsTail { root_path: PathBuf },
ScanRecipeBookLines { root_path: PathBuf },
}

View file

@ -0,0 +1,66 @@
use std::path::PathBuf;
use super::super::{CompareCommand, usage_error};
pub(super) fn parse_compare_command(
args: &[String],
) -> Result<CompareCommand, Box<dyn std::error::Error>> {
match args {
[subcommand, left_path, right_path] if subcommand == "compare-region-fixed-row-runs" => {
Ok(CompareCommand::CompareRegionFixedRowRuns {
left_path: left_path.into(),
right_path: right_path.into(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-classic-profile" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareClassicProfile {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-105-profile" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareRt3105Profile {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-candidate-table" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareCandidateTable {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-recipe-book-lines" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareRecipeBookLines {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-setup-payload-core" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareSetupPayloadCore {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-setup-launch-payload" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::CompareSetupLaunchPayload {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
[subcommand, smp_paths @ ..]
if subcommand == "compare-post-special-conditions-scalars" && smp_paths.len() >= 2 =>
{
Ok(CompareCommand::ComparePostSpecialConditionsScalars {
smp_paths: smp_paths.iter().map(PathBuf::from).collect(),
})
}
_ => usage_error(),
}
}

View file

@ -0,0 +1,73 @@
use super::super::{FixtureStateCommand, usage_error};
pub(super) fn parse_fixture_state_command(
args: &[String],
) -> Result<FixtureStateCommand, Box<dyn std::error::Error>> {
match args {
[subcommand, fixture_path] if subcommand == "validate-fixture" => {
Ok(FixtureStateCommand::ValidateFixture {
fixture_path: fixture_path.into(),
})
}
[subcommand, fixture_path] if subcommand == "summarize-fixture" => {
Ok(FixtureStateCommand::SummarizeFixture {
fixture_path: fixture_path.into(),
})
}
[subcommand, fixture_path, output_path] if subcommand == "export-fixture-state" => {
Ok(FixtureStateCommand::ExportFixtureState {
fixture_path: fixture_path.into(),
output_path: output_path.into(),
})
}
[subcommand, left_path, right_path] if subcommand == "diff-state" => {
Ok(FixtureStateCommand::DiffState {
left_path: left_path.into(),
right_path: right_path.into(),
})
}
[subcommand, snapshot_path] if subcommand == "summarize-state" => {
Ok(FixtureStateCommand::SummarizeState {
snapshot_path: snapshot_path.into(),
})
}
[subcommand, input_path, output_path] if subcommand == "snapshot-state" => {
Ok(FixtureStateCommand::SnapshotState {
input_path: input_path.into(),
output_path: output_path.into(),
})
}
[subcommand, smp_path] if subcommand == "summarize-save-load" => {
Ok(FixtureStateCommand::SummarizeSaveLoad {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "load-save-slice" => {
Ok(FixtureStateCommand::LoadSaveSlice {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path, output_path] if subcommand == "snapshot-save-state" => {
Ok(FixtureStateCommand::SnapshotSaveState {
smp_path: smp_path.into(),
output_path: output_path.into(),
})
}
[subcommand, smp_path, output_path] if subcommand == "export-save-slice" => {
Ok(FixtureStateCommand::ExportSaveSlice {
smp_path: smp_path.into(),
output_path: output_path.into(),
})
}
[subcommand, snapshot_path, save_slice_path, output_path]
if subcommand == "export-overlay-import" =>
{
Ok(FixtureStateCommand::ExportOverlayImport {
snapshot_path: snapshot_path.into(),
save_slice_path: save_slice_path.into(),
output_path: output_path.into(),
})
}
_ => usage_error(),
}
}

View file

@ -0,0 +1,146 @@
use super::super::{InspectCommand, usage_error};
pub(super) fn parse_inspect_command(
args: &[String],
) -> Result<InspectCommand, Box<dyn std::error::Error>> {
match args {
[subcommand, smp_path] if subcommand == "inspect-smp" => Ok(InspectCommand::InspectSmp {
smp_path: smp_path.into(),
}),
[subcommand, smp_path] if subcommand == "inspect-candidate-table" => {
Ok(InspectCommand::InspectCandidateTable {
smp_path: smp_path.into(),
})
}
[subcommand, root_path] if subcommand == "inspect-compact-event-dispatch-cluster" => {
Ok(InspectCommand::InspectCompactEventDispatchCluster {
root_path: root_path.into(),
})
}
[subcommand, root_path]
if subcommand == "inspect-compact-event-dispatch-cluster-counts" =>
{
Ok(InspectCommand::InspectCompactEventDispatchClusterCounts {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "inspect-map-title-hints" => {
Ok(InspectCommand::InspectMapTitleHints {
root_path: root_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-save-company-chairman" => {
Ok(InspectCommand::InspectSaveCompanyChairman {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-save-placed-structure-triplets" => {
Ok(InspectCommand::InspectSavePlacedStructureTriplets {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-periodic-company-service-trace" => {
Ok(InspectCommand::InspectPeriodicCompanyServiceTrace {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-region-service-trace" => {
Ok(InspectCommand::InspectRegionServiceTrace {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-infrastructure-asset-trace" => {
Ok(InspectCommand::InspectInfrastructureAssetTrace {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-save-region-queued-notice-records" => {
Ok(InspectCommand::InspectSaveRegionQueuedNoticeRecords {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-placed-structure-dynamic-side-buffer" => {
Ok(InspectCommand::InspectPlacedStructureDynamicSideBuffer {
smp_path: smp_path.into(),
})
}
[subcommand, smp_path] if subcommand == "inspect-unclassified-save-collections" => {
Ok(InspectCommand::InspectUnclassifiedSaveCollections {
smp_path: smp_path.into(),
})
}
[subcommand, pk4_path] if subcommand == "inspect-pk4" => Ok(InspectCommand::InspectPk4 {
pk4_path: pk4_path.into(),
}),
[subcommand, cargo_types_dir] if subcommand == "inspect-cargo-types" => {
Ok(InspectCommand::InspectCargoTypes {
cargo_types_dir: cargo_types_dir.into(),
})
}
[subcommand, building_types_dir] if subcommand == "inspect-building-type-sources" => {
Ok(InspectCommand::InspectBuildingTypeSources {
building_types_dir: building_types_dir.into(),
bindings_path: None,
})
}
[subcommand, building_types_dir, bindings_path]
if subcommand == "inspect-building-type-sources" =>
{
Ok(InspectCommand::InspectBuildingTypeSources {
building_types_dir: building_types_dir.into(),
bindings_path: Some(bindings_path.into()),
})
}
[subcommand, cargo_skin_pk4_path] if subcommand == "inspect-cargo-skins" => {
Ok(InspectCommand::InspectCargoSkins {
cargo_skin_pk4_path: cargo_skin_pk4_path.into(),
})
}
[subcommand, cargo_types_dir, cargo_skin_pk4_path]
if subcommand == "inspect-cargo-economy-sources" =>
{
Ok(InspectCommand::InspectCargoEconomySources {
cargo_types_dir: cargo_types_dir.into(),
cargo_skin_pk4_path: cargo_skin_pk4_path.into(),
})
}
[subcommand, cargo_types_dir, cargo_skin_pk4_path]
if subcommand == "inspect-cargo-production-selector" =>
{
Ok(InspectCommand::InspectCargoProductionSelector {
cargo_types_dir: cargo_types_dir.into(),
cargo_skin_pk4_path: cargo_skin_pk4_path.into(),
})
}
[subcommand, cargo_types_dir, cargo_skin_pk4_path]
if subcommand == "inspect-cargo-price-selector" =>
{
Ok(InspectCommand::InspectCargoPriceSelector {
cargo_types_dir: cargo_types_dir.into(),
cargo_skin_pk4_path: cargo_skin_pk4_path.into(),
})
}
[subcommand, win_path] if subcommand == "inspect-win" => Ok(InspectCommand::InspectWin {
win_path: win_path.into(),
}),
[subcommand, pk4_path, entry_name, output_path] if subcommand == "extract-pk4-entry" => {
Ok(InspectCommand::ExtractPk4Entry {
pk4_path: pk4_path.into(),
entry_name: entry_name.clone(),
output_path: output_path.into(),
})
}
[subcommand, exe_path] if subcommand == "inspect-campaign-exe" => {
Ok(InspectCommand::InspectCampaignExe {
exe_path: exe_path.into(),
})
}
[subcommand, smp_path, output_path] if subcommand == "export-profile-block" => {
Ok(InspectCommand::ExportProfileBlock {
smp_path: smp_path.into(),
output_path: output_path.into(),
})
}
_ => usage_error(),
}
}

View file

@ -0,0 +1,74 @@
mod compare;
mod fixture_state;
mod inspect;
mod scan;
use super::{Command, RuntimeCommand, usage_error};
pub(super) fn parse_runtime_command(
args: &[String],
) -> Result<Command, Box<dyn std::error::Error>> {
let [subcommand, ..] = args else {
return usage_error();
};
let runtime_command = match subcommand.as_str() {
"validate-fixture"
| "summarize-fixture"
| "export-fixture-state"
| "diff-state"
| "summarize-state"
| "snapshot-state"
| "summarize-save-load"
| "load-save-slice"
| "snapshot-save-state"
| "export-save-slice"
| "export-overlay-import" => {
RuntimeCommand::FixtureState(fixture_state::parse_fixture_state_command(args)?)
}
"inspect-smp"
| "inspect-candidate-table"
| "inspect-compact-event-dispatch-cluster"
| "inspect-compact-event-dispatch-cluster-counts"
| "inspect-map-title-hints"
| "inspect-save-company-chairman"
| "inspect-save-placed-structure-triplets"
| "inspect-periodic-company-service-trace"
| "inspect-region-service-trace"
| "inspect-infrastructure-asset-trace"
| "inspect-save-region-queued-notice-records"
| "inspect-placed-structure-dynamic-side-buffer"
| "inspect-unclassified-save-collections"
| "inspect-pk4"
| "inspect-cargo-types"
| "inspect-building-type-sources"
| "inspect-cargo-skins"
| "inspect-cargo-economy-sources"
| "inspect-cargo-production-selector"
| "inspect-cargo-price-selector"
| "inspect-win"
| "extract-pk4-entry"
| "inspect-campaign-exe"
| "export-profile-block" => RuntimeCommand::Inspect(inspect::parse_inspect_command(args)?),
"compare-region-fixed-row-runs"
| "compare-classic-profile"
| "compare-105-profile"
| "compare-candidate-table"
| "compare-recipe-book-lines"
| "compare-setup-payload-core"
| "compare-setup-launch-payload"
| "compare-post-special-conditions-scalars" => {
RuntimeCommand::Compare(compare::parse_compare_command(args)?)
}
"scan-candidate-table-headers"
| "scan-candidate-table-named-runs"
| "scan-special-conditions"
| "scan-aligned-runtime-rule-band"
| "scan-post-special-conditions-scalars"
| "scan-post-special-conditions-tail"
| "scan-recipe-book-lines" => RuntimeCommand::Scan(scan::parse_scan_command(args)?),
_ => return usage_error(),
};
Ok(Command::Runtime(runtime_command))
}

View file

@ -0,0 +1,44 @@
use super::super::{ScanCommand, usage_error};
pub(super) fn parse_scan_command(
args: &[String],
) -> Result<ScanCommand, Box<dyn std::error::Error>> {
match args {
[subcommand, root_path] if subcommand == "scan-candidate-table-headers" => {
Ok(ScanCommand::ScanCandidateTableHeaders {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-candidate-table-named-runs" => {
Ok(ScanCommand::ScanCandidateTableNamedRuns {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-special-conditions" => {
Ok(ScanCommand::ScanSpecialConditions {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-aligned-runtime-rule-band" => {
Ok(ScanCommand::ScanAlignedRuntimeRuleBand {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-post-special-conditions-scalars" => {
Ok(ScanCommand::ScanPostSpecialConditionsScalars {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-post-special-conditions-tail" => {
Ok(ScanCommand::ScanPostSpecialConditionsTail {
root_path: root_path.into(),
})
}
[subcommand, root_path] if subcommand == "scan-recipe-book-lines" => {
Ok(ScanCommand::ScanRecipeBookLines {
root_path: root_path.into(),
})
}
_ => usage_error(),
}
}

View file

@ -0,0 +1,18 @@
use std::path::Path;
use super::{Command, usage_error};
pub(super) fn parse_validate_command(
args: &[String],
current_dir: &Path,
) -> Result<Command, Box<dyn std::error::Error>> {
match args {
[] => Ok(Command::Validate {
repo_root: current_dir.to_path_buf(),
}),
[repo_root] => Ok(Command::Validate {
repo_root: repo_root.into(),
}),
_ => usage_error(),
}
}

View file

@ -0,0 +1,12 @@
use crate::app::command::FinanceCommand;
use crate::app::finance::{run_finance_diff, run_finance_eval};
pub(super) fn dispatch_finance(command: FinanceCommand) -> Result<(), Box<dyn std::error::Error>> {
match command {
FinanceCommand::Eval { snapshot_path } => run_finance_eval(&snapshot_path),
FinanceCommand::Diff {
left_path,
right_path,
} => run_finance_diff(&left_path, &right_path),
}
}

View file

@ -0,0 +1,81 @@
mod finance;
mod runtime;
mod validate;
use crate::app::command::{Command, parse_command};
pub(super) fn run() -> Result<(), Box<dyn std::error::Error>> {
dispatch_command(parse_command()?)
}
fn dispatch_command(command: Command) -> Result<(), Box<dyn std::error::Error>> {
match command {
Command::Validate { repo_root } => validate::dispatch_validate(repo_root),
Command::Finance(command) => finance::dispatch_finance(command),
Command::Runtime(command) => runtime::dispatch_runtime(command),
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use rrt_model::finance::FinanceSnapshot;
use super::dispatch_command;
use crate::app::command::{Command, FinanceCommand, FixtureStateCommand, RuntimeCommand};
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn unique_temp_path(stem: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
std::env::temp_dir().join(format!("rrt-cli-{stem}-{unique}.json"))
}
#[test]
fn dispatches_validate_command() {
dispatch_command(Command::Validate {
repo_root: workspace_root(),
})
.expect("validate dispatch should succeed");
}
#[test]
fn dispatches_finance_eval_command() {
let snapshot_path = unique_temp_path("finance-eval");
let snapshot = FinanceSnapshot {
policy: Default::default(),
company: Default::default(),
};
fs::write(
&snapshot_path,
serde_json::to_vec_pretty(&snapshot).expect("snapshot should serialize"),
)
.expect("snapshot should be written");
let result = dispatch_command(Command::Finance(FinanceCommand::Eval {
snapshot_path: snapshot_path.clone(),
}));
let _ = fs::remove_file(&snapshot_path);
result.expect("finance dispatch should succeed");
}
#[test]
fn dispatches_runtime_fixture_state_command() {
dispatch_command(Command::Runtime(RuntimeCommand::FixtureState(
FixtureStateCommand::SummarizeState {
snapshot_path: workspace_root()
.join("fixtures/runtime/minimal-world-state-input.json"),
},
)))
.expect("runtime fixture/state dispatch should succeed");
}
}

View file

@ -0,0 +1,30 @@
use crate::app::command::CompareCommand;
use crate::app::runtime_compare::{
compare_candidate_table, compare_classic_profile, compare_post_special_conditions_scalars,
compare_recipe_book_lines, compare_region_fixed_row_runs, compare_rt3_105_profile,
compare_setup_launch_payload, compare_setup_payload_core,
};
pub(super) fn dispatch_compare(command: CompareCommand) -> Result<(), Box<dyn std::error::Error>> {
match command {
CompareCommand::CompareRegionFixedRowRuns {
left_path,
right_path,
} => compare_region_fixed_row_runs(&left_path, &right_path),
CompareCommand::CompareClassicProfile { smp_paths } => compare_classic_profile(&smp_paths),
CompareCommand::CompareRt3105Profile { smp_paths } => compare_rt3_105_profile(&smp_paths),
CompareCommand::CompareCandidateTable { smp_paths } => compare_candidate_table(&smp_paths),
CompareCommand::CompareRecipeBookLines { smp_paths } => {
compare_recipe_book_lines(&smp_paths)
}
CompareCommand::CompareSetupPayloadCore { smp_paths } => {
compare_setup_payload_core(&smp_paths)
}
CompareCommand::CompareSetupLaunchPayload { smp_paths } => {
compare_setup_launch_payload(&smp_paths)
}
CompareCommand::ComparePostSpecialConditionsScalars { smp_paths } => {
compare_post_special_conditions_scalars(&smp_paths)
}
}
}

View file

@ -0,0 +1,43 @@
use crate::app::command::FixtureStateCommand;
use crate::app::runtime_fixture_state::{
diff_state, export_fixture_state, export_overlay_import, export_save_slice, load_save_slice,
snapshot_save_state, snapshot_state, summarize_fixture, summarize_save_load, summarize_state,
validate_fixture,
};
pub(super) fn dispatch_fixture_state(
command: FixtureStateCommand,
) -> Result<(), Box<dyn std::error::Error>> {
match command {
FixtureStateCommand::ValidateFixture { fixture_path } => validate_fixture(&fixture_path),
FixtureStateCommand::SummarizeFixture { fixture_path } => summarize_fixture(&fixture_path),
FixtureStateCommand::ExportFixtureState {
fixture_path,
output_path,
} => export_fixture_state(&fixture_path, &output_path),
FixtureStateCommand::DiffState {
left_path,
right_path,
} => diff_state(&left_path, &right_path),
FixtureStateCommand::SummarizeState { snapshot_path } => summarize_state(&snapshot_path),
FixtureStateCommand::SnapshotState {
input_path,
output_path,
} => snapshot_state(&input_path, &output_path),
FixtureStateCommand::SummarizeSaveLoad { smp_path } => summarize_save_load(&smp_path),
FixtureStateCommand::LoadSaveSlice { smp_path } => load_save_slice(&smp_path),
FixtureStateCommand::SnapshotSaveState {
smp_path,
output_path,
} => snapshot_save_state(&smp_path, &output_path),
FixtureStateCommand::ExportSaveSlice {
smp_path,
output_path,
} => export_save_slice(&smp_path, &output_path),
FixtureStateCommand::ExportOverlayImport {
snapshot_path,
save_slice_path,
output_path,
} => export_overlay_import(&snapshot_path, &save_slice_path, &output_path),
}
}

View file

@ -0,0 +1,85 @@
use crate::app::command::InspectCommand;
use crate::app::runtime_compare::inspect_candidate_table;
use crate::app::runtime_inspect::{
export_profile_block, extract_pk4_entry, inspect_building_type_sources, inspect_campaign_exe,
inspect_cargo_economy_sources, inspect_cargo_price_selector, inspect_cargo_production_selector,
inspect_cargo_skins, inspect_cargo_types, inspect_compact_event_dispatch_cluster,
inspect_compact_event_dispatch_cluster_counts, inspect_infrastructure_asset_trace,
inspect_map_title_hints, inspect_periodic_company_service_trace, inspect_pk4,
inspect_placed_structure_dynamic_side_buffer, inspect_region_service_trace,
inspect_save_company_chairman, inspect_save_placed_structure_triplets,
inspect_save_region_queued_notice_records, inspect_smp, inspect_unclassified_save_collections,
inspect_win,
};
pub(super) fn dispatch_inspect(command: InspectCommand) -> Result<(), Box<dyn std::error::Error>> {
match command {
InspectCommand::InspectSmp { smp_path } => inspect_smp(&smp_path),
InspectCommand::InspectCandidateTable { smp_path } => inspect_candidate_table(&smp_path),
InspectCommand::InspectCompactEventDispatchCluster { root_path } => {
inspect_compact_event_dispatch_cluster(&root_path)
}
InspectCommand::InspectCompactEventDispatchClusterCounts { root_path } => {
inspect_compact_event_dispatch_cluster_counts(&root_path)
}
InspectCommand::InspectMapTitleHints { root_path } => inspect_map_title_hints(&root_path),
InspectCommand::InspectSaveCompanyChairman { smp_path } => {
inspect_save_company_chairman(&smp_path)
}
InspectCommand::InspectSavePlacedStructureTriplets { smp_path } => {
inspect_save_placed_structure_triplets(&smp_path)
}
InspectCommand::InspectPeriodicCompanyServiceTrace { smp_path } => {
inspect_periodic_company_service_trace(&smp_path)
}
InspectCommand::InspectRegionServiceTrace { smp_path } => {
inspect_region_service_trace(&smp_path)
}
InspectCommand::InspectInfrastructureAssetTrace { smp_path } => {
inspect_infrastructure_asset_trace(&smp_path)
}
InspectCommand::InspectSaveRegionQueuedNoticeRecords { smp_path } => {
inspect_save_region_queued_notice_records(&smp_path)
}
InspectCommand::InspectPlacedStructureDynamicSideBuffer { smp_path } => {
inspect_placed_structure_dynamic_side_buffer(&smp_path)
}
InspectCommand::InspectUnclassifiedSaveCollections { smp_path } => {
inspect_unclassified_save_collections(&smp_path)
}
InspectCommand::InspectPk4 { pk4_path } => inspect_pk4(&pk4_path),
InspectCommand::InspectCargoTypes { cargo_types_dir } => {
inspect_cargo_types(&cargo_types_dir)
}
InspectCommand::InspectBuildingTypeSources {
building_types_dir,
bindings_path,
} => inspect_building_type_sources(&building_types_dir, bindings_path.as_deref()),
InspectCommand::InspectCargoSkins {
cargo_skin_pk4_path,
} => inspect_cargo_skins(&cargo_skin_pk4_path),
InspectCommand::InspectCargoEconomySources {
cargo_types_dir,
cargo_skin_pk4_path,
} => inspect_cargo_economy_sources(&cargo_types_dir, &cargo_skin_pk4_path),
InspectCommand::InspectCargoProductionSelector {
cargo_types_dir,
cargo_skin_pk4_path,
} => inspect_cargo_production_selector(&cargo_types_dir, &cargo_skin_pk4_path),
InspectCommand::InspectCargoPriceSelector {
cargo_types_dir,
cargo_skin_pk4_path,
} => inspect_cargo_price_selector(&cargo_types_dir, &cargo_skin_pk4_path),
InspectCommand::InspectWin { win_path } => inspect_win(&win_path),
InspectCommand::ExtractPk4Entry {
pk4_path,
entry_name,
output_path,
} => extract_pk4_entry(&pk4_path, &entry_name, &output_path),
InspectCommand::InspectCampaignExe { exe_path } => inspect_campaign_exe(&exe_path),
InspectCommand::ExportProfileBlock {
smp_path,
output_path,
} => export_profile_block(&smp_path, &output_path),
}
}

View file

@ -0,0 +1,15 @@
mod compare;
mod fixture_state;
mod inspect;
mod scan;
use crate::app::command::RuntimeCommand;
pub(super) fn dispatch_runtime(command: RuntimeCommand) -> Result<(), Box<dyn std::error::Error>> {
match command {
RuntimeCommand::FixtureState(command) => fixture_state::dispatch_fixture_state(command),
RuntimeCommand::Inspect(command) => inspect::dispatch_inspect(command),
RuntimeCommand::Compare(command) => compare::dispatch_compare(command),
RuntimeCommand::Scan(command) => scan::dispatch_scan(command),
}
}

View file

@ -0,0 +1,28 @@
use crate::app::command::ScanCommand;
use crate::app::runtime_scan::{
scan_aligned_runtime_rule_band, scan_candidate_table_headers, scan_candidate_table_named_runs,
scan_post_special_conditions_scalars, scan_post_special_conditions_tail,
scan_recipe_book_lines, scan_special_conditions,
};
pub(super) fn dispatch_scan(command: ScanCommand) -> Result<(), Box<dyn std::error::Error>> {
match command {
ScanCommand::ScanCandidateTableHeaders { root_path } => {
scan_candidate_table_headers(&root_path)
}
ScanCommand::ScanCandidateTableNamedRuns { root_path } => {
scan_candidate_table_named_runs(&root_path)
}
ScanCommand::ScanSpecialConditions { root_path } => scan_special_conditions(&root_path),
ScanCommand::ScanAlignedRuntimeRuleBand { root_path } => {
scan_aligned_runtime_rule_band(&root_path)
}
ScanCommand::ScanPostSpecialConditionsScalars { root_path } => {
scan_post_special_conditions_scalars(&root_path)
}
ScanCommand::ScanPostSpecialConditionsTail { root_path } => {
scan_post_special_conditions_tail(&root_path)
}
ScanCommand::ScanRecipeBookLines { root_path } => scan_recipe_book_lines(&root_path),
}
}

View file

@ -0,0 +1,15 @@
use std::path::PathBuf;
use crate::app::validate::{
validate_binary_summary, validate_control_loop_atlas, validate_function_map,
validate_required_files,
};
pub(super) fn dispatch_validate(repo_root: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
validate_required_files(&repo_root)?;
validate_binary_summary(&repo_root)?;
validate_function_map(&repo_root)?;
validate_control_loop_atlas(&repo_root)?;
println!("baseline validation passed");
Ok(())
}

View file

@ -0,0 +1,110 @@
use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
use rrt_model::finance::{FinanceOutcome, FinanceSnapshot};
use serde_json::Value;
use crate::app::reports::state::{FinanceDiffEntry, FinanceDiffReport};
pub(crate) fn run_finance_eval(snapshot_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let outcome = load_finance_outcome(snapshot_path)?;
println!("{}", serde_json::to_string_pretty(&outcome)?);
Ok(())
}
pub(crate) fn run_finance_diff(
left_path: &Path,
right_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let left = load_finance_outcome(left_path)?;
let right = load_finance_outcome(right_path)?;
let report = diff_finance_outcomes(&left, &right)?;
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_finance_outcome(
path: &Path,
) -> Result<FinanceOutcome, Box<dyn std::error::Error>> {
let text = fs::read_to_string(path)?;
if let Ok(snapshot) = serde_json::from_str::<FinanceSnapshot>(&text) {
return Ok(snapshot.evaluate());
}
if let Ok(outcome) = serde_json::from_str::<FinanceOutcome>(&text) {
return Ok(outcome);
}
Err(format!(
"unable to parse {} as FinanceSnapshot or FinanceOutcome",
path.display()
)
.into())
}
pub(crate) fn diff_finance_outcomes(
left: &FinanceOutcome,
right: &FinanceOutcome,
) -> Result<FinanceDiffReport, Box<dyn std::error::Error>> {
let left_value = serde_json::to_value(left)?;
let right_value = serde_json::to_value(right)?;
let mut differences = Vec::new();
collect_json_differences("$", &left_value, &right_value, &mut differences);
Ok(FinanceDiffReport {
matches: differences.is_empty(),
difference_count: differences.len(),
differences,
})
}
fn collect_json_differences(
path: &str,
left: &Value,
right: &Value,
differences: &mut Vec<FinanceDiffEntry>,
) {
match (left, right) {
(Value::Object(left_map), Value::Object(right_map)) => {
let mut keys = BTreeSet::new();
keys.extend(left_map.keys().cloned());
keys.extend(right_map.keys().cloned());
for key in keys {
let next_path = format!("{path}.{key}");
match (left_map.get(&key), right_map.get(&key)) {
(Some(left_value), Some(right_value)) => {
collect_json_differences(&next_path, left_value, right_value, differences);
}
(left_value, right_value) => differences.push(FinanceDiffEntry {
path: next_path,
left: left_value.cloned().unwrap_or(Value::Null),
right: right_value.cloned().unwrap_or(Value::Null),
}),
}
}
}
(Value::Array(left_items), Value::Array(right_items)) => {
let max_len = left_items.len().max(right_items.len());
for index in 0..max_len {
let next_path = format!("{path}[{index}]");
match (left_items.get(index), right_items.get(index)) {
(Some(left_value), Some(right_value)) => {
collect_json_differences(&next_path, left_value, right_value, differences);
}
(left_value, right_value) => differences.push(FinanceDiffEntry {
path: next_path,
left: left_value.cloned().unwrap_or(Value::Null),
right: right_value.cloned().unwrap_or(Value::Null),
}),
}
}
}
_ if left != right => differences.push(FinanceDiffEntry {
path: path.to_string(),
left: left.clone(),
right: right.clone(),
}),
_ => {}
}
}

View file

@ -0,0 +1,446 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use crate::app::reports::inspect::{
RuntimeCompactEventDispatchClusterConditionTuple, RuntimeCompactEventDispatchClusterOccurrence,
RuntimeCompactEventDispatchClusterReport, RuntimeCompactEventDispatchClusterRow,
RuntimeProfileBlockExportDocument,
};
use rrt_runtime::inspect::smp::bundle::{SmpInspectionReport, inspect_smp_file};
pub(crate) fn build_runtime_compact_event_dispatch_cluster_report(
root_path: &Path,
) -> Result<RuntimeCompactEventDispatchClusterReport, Box<dyn std::error::Error>> {
let mut input_paths = Vec::new();
collect_compact_event_dispatch_cluster_input_paths(root_path, &mut input_paths)?;
input_paths.sort();
let mut maps_with_event_runtime_collection = 0usize;
let mut maps_with_dispatch_strip_records = 0usize;
let mut dispatch_strip_record_count = 0usize;
let mut dispatch_strip_records_with_trigger_kind = 0usize;
let mut dispatch_strip_records_missing_trigger_kind = 0usize;
let mut dispatch_strip_payload_families = BTreeMap::<String, usize>::new();
let mut dispatch_descriptor_occurrence_counts = BTreeMap::<String, usize>::new();
let mut dispatch_descriptor_map_counts = BTreeMap::<String, usize>::new();
let mut add_building_dispatch_record_count = 0usize;
let mut add_building_dispatch_records_with_trigger_kind = 0usize;
let mut add_building_dispatch_records_missing_trigger_kind = 0usize;
let mut add_building_descriptor_occurrence_counts = BTreeMap::<String, usize>::new();
let mut add_building_descriptor_map_counts = BTreeMap::<String, usize>::new();
let mut add_building_row_shape_occurrence_counts = BTreeMap::<String, usize>::new();
let mut add_building_row_shape_map_counts = BTreeMap::<String, usize>::new();
let mut add_building_signature_family_occurrence_counts = BTreeMap::<String, usize>::new();
let mut add_building_signature_family_map_counts = BTreeMap::<String, usize>::new();
let mut add_building_condition_tuple_occurrence_counts = BTreeMap::<String, usize>::new();
let mut add_building_condition_tuple_map_counts = BTreeMap::<String, usize>::new();
let mut add_building_signature_condition_cluster_occurrence_counts =
BTreeMap::<String, usize>::new();
let mut add_building_signature_condition_cluster_map_counts = BTreeMap::<String, usize>::new();
let mut signature_condition_cluster_descriptor_keys =
BTreeMap::<String, BTreeSet<String>>::new();
let mut add_building_signature_condition_clusters = BTreeSet::<String>::new();
let mut dispatch_descriptor_occurrences =
BTreeMap::<String, Vec<RuntimeCompactEventDispatchClusterOccurrence>>::new();
let mut unknown_descriptor_occurrences =
BTreeMap::<u32, Vec<RuntimeCompactEventDispatchClusterOccurrence>>::new();
for path in &input_paths {
let inspection = inspect_smp_file(path)?;
let Some(summary) = inspection.event_runtime_collection_summary else {
continue;
};
maps_with_event_runtime_collection += 1;
let mut map_dispatch_strip_record_count = 0usize;
let mut map_descriptor_keys = BTreeSet::<String>::new();
let mut map_add_building_descriptor_keys = BTreeSet::<String>::new();
let mut map_add_building_row_shapes = BTreeSet::<String>::new();
let mut map_add_building_signature_families = BTreeSet::<String>::new();
let mut map_add_building_condition_tuples = BTreeSet::<String>::new();
let mut map_add_building_signature_condition_clusters = BTreeSet::<String>::new();
for record in &summary.records {
let matching_rows = record
.grouped_effect_rows
.iter()
.filter(|row| compact_event_dispatch_strip_opcode(row.opcode))
.fold(
BTreeMap::<u32, Vec<RuntimeCompactEventDispatchClusterRow>>::new(),
|mut grouped, row| {
grouped.entry(row.descriptor_id).or_default().push(
RuntimeCompactEventDispatchClusterRow {
group_index: row.group_index,
descriptor_id: row.descriptor_id,
descriptor_label: row.descriptor_label.clone(),
opcode: row.opcode,
raw_scalar_value: row.raw_scalar_value,
},
);
grouped
},
);
if matching_rows.is_empty() {
continue;
}
map_dispatch_strip_record_count += 1;
if record.trigger_kind.is_some() {
dispatch_strip_records_with_trigger_kind += 1;
} else {
dispatch_strip_records_missing_trigger_kind += 1;
}
*dispatch_strip_payload_families
.entry(record.payload_family.clone())
.or_insert(0) += 1;
let mut record_has_add_building = false;
let condition_tuples = record
.standalone_condition_rows
.iter()
.map(|row| RuntimeCompactEventDispatchClusterConditionTuple {
raw_condition_id: row.raw_condition_id,
subtype: row.subtype,
metric: row.metric.clone(),
})
.collect::<Vec<_>>();
let signature_family = compact_event_signature_family_from_notes(&record.notes);
let condition_tuple_family =
compact_event_dispatch_condition_tuple_family(&condition_tuples);
let row_shape_family = compact_event_dispatch_row_shape_family(&matching_rows);
let signature_family_key = signature_family
.clone()
.unwrap_or_else(|| "unknown-signature-family".to_string());
let signature_condition_cluster_key =
compact_event_dispatch_signature_condition_cluster_key(
signature_family.as_deref(),
&condition_tuples,
);
for (descriptor_id, rows) in matching_rows {
let occurrence = RuntimeCompactEventDispatchClusterOccurrence {
path: path.display().to_string(),
record_index: record.record_index,
live_entry_id: record.live_entry_id,
payload_family: record.payload_family.clone(),
trigger_kind: record.trigger_kind,
signature_family: signature_family.clone(),
condition_tuples: condition_tuples.clone(),
rows: rows.clone(),
};
let descriptor_key = compact_event_dispatch_descriptor_key(descriptor_id, &rows);
signature_condition_cluster_descriptor_keys
.entry(signature_condition_cluster_key.clone())
.or_default()
.insert(descriptor_key.clone());
*dispatch_descriptor_occurrence_counts
.entry(descriptor_key.clone())
.or_insert(0) += 1;
map_descriptor_keys.insert(descriptor_key.clone());
if compact_event_dispatch_add_building_descriptor_id(descriptor_id) {
record_has_add_building = true;
add_building_signature_condition_clusters
.insert(signature_condition_cluster_key.clone());
*add_building_descriptor_occurrence_counts
.entry(descriptor_key.clone())
.or_insert(0) += 1;
map_add_building_descriptor_keys.insert(descriptor_key.clone());
*add_building_row_shape_occurrence_counts
.entry(row_shape_family.clone())
.or_insert(0) += 1;
map_add_building_row_shapes.insert(row_shape_family.clone());
*add_building_signature_family_occurrence_counts
.entry(signature_family_key.clone())
.or_insert(0) += 1;
*add_building_condition_tuple_occurrence_counts
.entry(condition_tuple_family.clone())
.or_insert(0) += 1;
*add_building_signature_condition_cluster_occurrence_counts
.entry(signature_condition_cluster_key.clone())
.or_insert(0) += 1;
map_add_building_signature_families.insert(signature_family_key.clone());
map_add_building_condition_tuples.insert(condition_tuple_family.clone());
map_add_building_signature_condition_clusters
.insert(signature_condition_cluster_key.clone());
}
dispatch_descriptor_occurrences
.entry(descriptor_key)
.or_default()
.push(occurrence.clone());
if rows.iter().all(|row| row.descriptor_label.is_none()) {
unknown_descriptor_occurrences
.entry(descriptor_id)
.or_default()
.push(occurrence);
}
}
if record_has_add_building {
add_building_dispatch_record_count += 1;
if record.trigger_kind.is_some() {
add_building_dispatch_records_with_trigger_kind += 1;
} else {
add_building_dispatch_records_missing_trigger_kind += 1;
}
}
}
if map_dispatch_strip_record_count > 0 {
maps_with_dispatch_strip_records += 1;
dispatch_strip_record_count += map_dispatch_strip_record_count;
}
for descriptor_key in map_descriptor_keys {
*dispatch_descriptor_map_counts
.entry(descriptor_key)
.or_insert(0) += 1;
}
for descriptor_key in map_add_building_descriptor_keys {
*add_building_descriptor_map_counts
.entry(descriptor_key)
.or_insert(0) += 1;
}
for row_shape in map_add_building_row_shapes {
*add_building_row_shape_map_counts
.entry(row_shape)
.or_insert(0) += 1;
}
for signature_family in map_add_building_signature_families {
*add_building_signature_family_map_counts
.entry(signature_family)
.or_insert(0) += 1;
}
for condition_tuple_family in map_add_building_condition_tuples {
*add_building_condition_tuple_map_counts
.entry(condition_tuple_family)
.or_insert(0) += 1;
}
for signature_condition_cluster in map_add_building_signature_condition_clusters {
*add_building_signature_condition_cluster_map_counts
.entry(signature_condition_cluster)
.or_insert(0) += 1;
}
}
let unknown_descriptor_ids = unknown_descriptor_occurrences
.keys()
.copied()
.collect::<Vec<_>>();
let unknown_descriptor_special_condition_label_matches = unknown_descriptor_ids
.iter()
.filter_map(|descriptor_id| {
special_condition_label_for_compact_dispatch_descriptor(*descriptor_id)
.map(|label| format!("{descriptor_id} -> {label}"))
})
.collect::<Vec<_>>();
let add_building_signature_condition_cluster_descriptor_keys =
add_building_signature_condition_clusters
.iter()
.map(|cluster| {
let keys = signature_condition_cluster_descriptor_keys
.get(cluster)
.map(|keys| keys.iter().cloned().collect::<Vec<_>>())
.unwrap_or_default();
(cluster.clone(), keys)
})
.collect::<BTreeMap<_, _>>();
let add_building_signature_condition_cluster_non_add_building_descriptor_keys =
add_building_signature_condition_cluster_descriptor_keys
.iter()
.map(|(cluster, keys)| {
let filtered = keys
.iter()
.filter(|key| !key.contains("Add Building"))
.cloned()
.collect::<Vec<_>>();
(cluster.clone(), filtered)
})
.collect::<BTreeMap<_, _>>();
Ok(RuntimeCompactEventDispatchClusterReport {
maps_scanned: input_paths.len(),
maps_with_event_runtime_collection,
maps_with_dispatch_strip_records,
dispatch_strip_record_count,
dispatch_strip_records_with_trigger_kind,
dispatch_strip_records_missing_trigger_kind,
dispatch_strip_payload_families,
dispatch_descriptor_occurrence_counts,
dispatch_descriptor_map_counts,
dispatch_descriptor_occurrences,
unknown_descriptor_ids,
unknown_descriptor_special_condition_label_matches,
unknown_descriptor_occurrences,
add_building_dispatch_record_count,
add_building_dispatch_records_with_trigger_kind,
add_building_dispatch_records_missing_trigger_kind,
add_building_descriptor_occurrence_counts,
add_building_descriptor_map_counts,
add_building_row_shape_occurrence_counts,
add_building_row_shape_map_counts,
add_building_signature_family_occurrence_counts,
add_building_signature_family_map_counts,
add_building_condition_tuple_occurrence_counts,
add_building_condition_tuple_map_counts,
add_building_signature_condition_cluster_occurrence_counts,
add_building_signature_condition_cluster_map_counts,
add_building_signature_condition_cluster_descriptor_keys,
add_building_signature_condition_cluster_non_add_building_descriptor_keys,
})
}
pub(crate) fn build_profile_block_export_document(
smp_path: &Path,
inspection: &SmpInspectionReport,
) -> Result<RuntimeProfileBlockExportDocument, Box<dyn std::error::Error>> {
if let Some(probe) = &inspection.classic_rehydrate_profile_probe {
return Ok(RuntimeProfileBlockExportDocument {
source_path: smp_path.display().to_string(),
profile_kind: "classic-rehydrate-profile".to_string(),
profile_family: probe.profile_family.clone(),
payload: serde_json::to_value(probe)?,
});
}
if let Some(probe) = &inspection.rt3_105_packed_profile_probe {
return Ok(RuntimeProfileBlockExportDocument {
source_path: smp_path.display().to_string(),
profile_kind: "rt3-105-packed-profile".to_string(),
profile_family: probe.profile_family.clone(),
payload: serde_json::to_value(probe)?,
});
}
Err(format!(
"{} did not expose an exportable packed-profile block",
smp_path.display()
)
.into())
}
pub(crate) fn compact_event_dispatch_add_building_descriptor_id(descriptor_id: u32) -> bool {
(503..=613).contains(&descriptor_id)
}
pub(crate) fn compact_event_dispatch_strip_opcode(opcode: u8) -> bool {
matches!(opcode, 0x04..=0x08 | 0x0d | 0x10..=0x13 | 0x16)
}
pub(crate) fn compact_event_signature_family_from_notes(notes: &[String]) -> Option<String> {
notes.iter().find_map(|note| {
note.strip_prefix("compact signature family = ")
.map(ToString::to_string)
})
}
pub(crate) fn special_condition_label_for_compact_dispatch_descriptor(
descriptor_id: u32,
) -> Option<&'static str> {
let band_index = descriptor_id.checked_sub(535)? as usize;
crate::app::runtime_scan::common::SPECIAL_CONDITION_LABELS
.get(band_index)
.copied()
}
pub(crate) fn collect_compact_event_dispatch_cluster_input_paths(
root_path: &Path,
out: &mut Vec<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
let metadata = match fs::symlink_metadata(root_path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
if metadata.file_type().is_symlink() {
return Ok(());
}
if root_path.is_file() {
if root_path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("gmp"))
{
out.push(root_path.to_path_buf());
}
return Ok(());
}
let entries = match fs::read_dir(root_path) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_compact_event_dispatch_cluster_input_paths(&path, out)?;
continue;
}
if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("gmp"))
{
out.push(path);
}
}
Ok(())
}
pub(crate) fn compact_event_dispatch_descriptor_key(
descriptor_id: u32,
rows: &[RuntimeCompactEventDispatchClusterRow],
) -> String {
rows.first()
.and_then(|row| row.descriptor_label.as_deref())
.map(|label| format!("{descriptor_id} {label}"))
.unwrap_or_else(|| descriptor_id.to_string())
}
pub(crate) fn compact_event_dispatch_row_shape_family(
grouped_rows: &BTreeMap<u32, Vec<RuntimeCompactEventDispatchClusterRow>>,
) -> String {
let mut parts = grouped_rows
.values()
.flat_map(|rows| rows.iter())
.map(|row| {
format!(
"{}:{}:{}",
row.group_index, row.opcode, row.raw_scalar_value
)
})
.collect::<Vec<_>>();
if parts.is_empty() {
return "[]".to_string();
}
parts.sort();
format!("[{}]", parts.join(","))
}
pub(crate) fn compact_event_dispatch_condition_tuple_family(
tuples: &[RuntimeCompactEventDispatchClusterConditionTuple],
) -> String {
if tuples.is_empty() {
return "[]".to_string();
}
let parts = tuples
.iter()
.map(|tuple| match &tuple.metric {
Some(metric) => format!("{}:{}:{}", tuple.raw_condition_id, tuple.subtype, metric),
None => format!("{}:{}", tuple.raw_condition_id, tuple.subtype),
})
.collect::<Vec<_>>();
format!("[{}]", parts.join(","))
}
pub(crate) fn compact_event_dispatch_signature_condition_cluster_key(
signature_family: Option<&str>,
tuples: &[RuntimeCompactEventDispatchClusterConditionTuple],
) -> String {
format!(
"{} :: {}",
signature_family.unwrap_or("unknown-signature-family"),
compact_event_dispatch_condition_tuple_family(tuples)
)
}

View file

@ -0,0 +1,2 @@
pub(super) mod inspect;
pub(super) mod state_io;

View file

@ -0,0 +1,102 @@
use std::path::Path;
use crate::app::reports::state::{RuntimeOverlayImportExportOutput, RuntimeSaveSliceExportOutput};
use rrt_fixtures::{FixtureValidationReport, normalize_runtime_state};
use rrt_runtime::documents::{
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource,
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, load_runtime_state_input,
save_runtime_overlay_import_document, save_runtime_save_slice_document,
};
use rrt_runtime::inspect::smp::save_load::SmpLoadedSaveSlice;
use rrt_runtime::persistence::{
load_runtime_snapshot_document, validate_runtime_snapshot_document,
};
use serde_json::Value;
pub(crate) fn load_normalized_runtime_state(
path: &Path,
) -> Result<Value, Box<dyn std::error::Error>> {
if let Ok(snapshot) = load_runtime_snapshot_document(path) {
validate_runtime_snapshot_document(&snapshot)
.map_err(|err| format!("invalid runtime snapshot: {err}"))?;
return normalize_runtime_state(&snapshot.state);
}
let input = load_runtime_state_input(path)?;
normalize_runtime_state(&input.state)
}
pub(crate) fn export_runtime_save_slice_document(
smp_path: &Path,
output_path: &Path,
save_slice: SmpLoadedSaveSlice,
) -> Result<RuntimeSaveSliceExportOutput, Box<dyn std::error::Error>> {
let document = RuntimeSaveSliceDocument {
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
save_slice_id: smp_path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("save-slice")
.to_string(),
source: RuntimeSaveSliceDocumentSource {
description: Some(format!(
"Exported loaded save slice from {}",
smp_path.display()
)),
original_save_filename: smp_path
.file_name()
.and_then(|name| name.to_str())
.map(ToString::to_string),
original_save_sha256: None,
notes: vec![],
},
save_slice,
};
save_runtime_save_slice_document(output_path, &document)?;
Ok(RuntimeSaveSliceExportOutput {
path: smp_path.display().to_string(),
output_path: output_path.display().to_string(),
save_slice_id: document.save_slice_id,
})
}
pub(crate) fn export_runtime_overlay_import_document(
snapshot_path: &Path,
save_slice_path: &Path,
output_path: &Path,
) -> Result<RuntimeOverlayImportExportOutput, Box<dyn std::error::Error>> {
let import_id = output_path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("overlay-import")
.to_string();
let document = RuntimeOverlayImportDocument {
format_version: OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION,
import_id: import_id.clone(),
source: RuntimeOverlayImportDocumentSource {
description: Some(format!(
"Overlay import referencing {} and {}",
snapshot_path.display(),
save_slice_path.display()
)),
notes: vec![],
},
base_snapshot_path: snapshot_path.display().to_string(),
save_slice_path: save_slice_path.display().to_string(),
};
save_runtime_overlay_import_document(output_path, &document)?;
Ok(RuntimeOverlayImportExportOutput {
output_path: output_path.display().to_string(),
import_id,
base_snapshot_path: document.base_snapshot_path,
save_slice_path: document.save_slice_path,
})
}
pub(crate) fn print_runtime_validation_report(
report: &FixtureValidationReport,
) -> Result<(), Box<dyn std::error::Error>> {
println!("{}", serde_json::to_string_pretty(report)?);
Ok(())
}

View file

@ -0,0 +1,16 @@
mod command;
mod dispatch;
mod finance;
mod helpers;
mod reports;
mod runtime_compare;
mod runtime_fixture_state;
mod runtime_inspect;
mod runtime_scan;
#[cfg(test)]
mod tests;
mod validate;
pub(crate) fn run() -> Result<(), Box<dyn std::error::Error>> {
dispatch::run()
}

View file

@ -0,0 +1,258 @@
use std::collections::BTreeMap;
use rrt_runtime::inspect::{
building::BuildingTypeSourceReport,
campaign::CampaignExeInspectionReport,
cargo::{
CargoEconomySourceReport, CargoSelectorReport, CargoSkinInspectionReport,
CargoTypeInspectionReport,
},
pk4::{Pk4ExtractionReport, Pk4InspectionReport},
smp::{
bundle::SmpInspectionReport,
map_title::SmpMapTitleHintProbe,
services::{
SmpInfrastructureAssetTraceReport, SmpPeriodicCompanyServiceTraceReport,
SmpRegionServiceTraceReport,
},
world::SmpSaveCompanyChairmanAnalysisReport,
},
win::WinInspectionReport,
};
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSmpInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: SmpInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterOutput {
pub(crate) root_path: String,
pub(crate) report: RuntimeCompactEventDispatchClusterReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterCountsOutput {
pub(crate) root_path: String,
pub(crate) report: RuntimeCompactEventDispatchClusterCountsReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeMapTitleHintDirectoryOutput {
pub(crate) root_path: String,
pub(crate) report: RuntimeMapTitleHintDirectoryReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeMapTitleHintDirectoryReport {
pub(crate) maps_scanned: usize,
pub(crate) maps_with_probe: usize,
pub(crate) maps_with_grounded_title_hits: usize,
pub(crate) maps_with_adjacent_title_pairs: usize,
pub(crate) maps_with_same_stem_adjacent_pairs: usize,
pub(crate) maps: Vec<RuntimeMapTitleHintMapEntry>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeMapTitleHintMapEntry {
pub(crate) path: String,
pub(crate) probe: SmpMapTitleHintProbe,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterReport {
pub(crate) maps_scanned: usize,
pub(crate) maps_with_event_runtime_collection: usize,
pub(crate) maps_with_dispatch_strip_records: usize,
pub(crate) dispatch_strip_record_count: usize,
pub(crate) dispatch_strip_records_with_trigger_kind: usize,
pub(crate) dispatch_strip_records_missing_trigger_kind: usize,
pub(crate) dispatch_strip_payload_families: BTreeMap<String, usize>,
pub(crate) dispatch_descriptor_occurrence_counts: BTreeMap<String, usize>,
pub(crate) dispatch_descriptor_map_counts: BTreeMap<String, usize>,
pub(crate) dispatch_descriptor_occurrences:
BTreeMap<String, Vec<RuntimeCompactEventDispatchClusterOccurrence>>,
pub(crate) unknown_descriptor_ids: Vec<u32>,
pub(crate) unknown_descriptor_special_condition_label_matches: Vec<String>,
pub(crate) unknown_descriptor_occurrences:
BTreeMap<u32, Vec<RuntimeCompactEventDispatchClusterOccurrence>>,
pub(crate) add_building_dispatch_record_count: usize,
pub(crate) add_building_dispatch_records_with_trigger_kind: usize,
pub(crate) add_building_dispatch_records_missing_trigger_kind: usize,
pub(crate) add_building_descriptor_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_descriptor_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_row_shape_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_row_shape_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_family_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_family_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_condition_tuple_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_condition_tuple_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_descriptor_keys:
BTreeMap<String, Vec<String>>,
pub(crate) add_building_signature_condition_cluster_non_add_building_descriptor_keys:
BTreeMap<String, Vec<String>>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterCountsReport {
pub(crate) maps_scanned: usize,
pub(crate) maps_with_event_runtime_collection: usize,
pub(crate) maps_with_dispatch_strip_records: usize,
pub(crate) dispatch_strip_record_count: usize,
pub(crate) dispatch_strip_records_with_trigger_kind: usize,
pub(crate) dispatch_strip_records_missing_trigger_kind: usize,
pub(crate) dispatch_strip_payload_families: BTreeMap<String, usize>,
pub(crate) dispatch_descriptor_occurrence_counts: BTreeMap<String, usize>,
pub(crate) dispatch_descriptor_map_counts: BTreeMap<String, usize>,
pub(crate) unknown_descriptor_ids: Vec<u32>,
pub(crate) unknown_descriptor_special_condition_label_matches: Vec<String>,
pub(crate) add_building_dispatch_record_count: usize,
pub(crate) add_building_dispatch_records_with_trigger_kind: usize,
pub(crate) add_building_dispatch_records_missing_trigger_kind: usize,
pub(crate) add_building_descriptor_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_descriptor_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_row_shape_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_row_shape_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_family_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_family_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_condition_tuple_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_condition_tuple_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_occurrence_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_map_counts: BTreeMap<String, usize>,
pub(crate) add_building_signature_condition_cluster_descriptor_keys:
BTreeMap<String, Vec<String>>,
pub(crate) add_building_signature_condition_cluster_non_add_building_descriptor_keys:
BTreeMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterOccurrence {
pub(crate) path: String,
pub(crate) record_index: usize,
pub(crate) live_entry_id: u32,
pub(crate) payload_family: String,
pub(crate) trigger_kind: Option<u8>,
pub(crate) signature_family: Option<String>,
pub(crate) condition_tuples: Vec<RuntimeCompactEventDispatchClusterConditionTuple>,
pub(crate) rows: Vec<RuntimeCompactEventDispatchClusterRow>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterConditionTuple {
pub(crate) raw_condition_id: i32,
pub(crate) subtype: u8,
pub(crate) metric: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCompactEventDispatchClusterRow {
pub(crate) group_index: usize,
pub(crate) descriptor_id: u32,
pub(crate) descriptor_label: Option<String>,
pub(crate) opcode: u8,
pub(crate) raw_scalar_value: i32,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSaveCompanyChairmanAnalysisOutput {
pub(crate) path: String,
pub(crate) analysis: SmpSaveCompanyChairmanAnalysisReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePeriodicCompanyServiceTraceOutput {
pub(crate) path: String,
pub(crate) trace: SmpPeriodicCompanyServiceTraceReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRegionServiceTraceOutput {
pub(crate) path: String,
pub(crate) trace: SmpRegionServiceTraceReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeInfrastructureAssetTraceOutput {
pub(crate) path: String,
pub(crate) trace: SmpInfrastructureAssetTraceReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePk4InspectionOutput {
pub(crate) path: String,
pub(crate) inspection: Pk4InspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCargoTypeInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: CargoTypeInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeBuildingTypeInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: BuildingTypeSourceReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCargoSkinInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: CargoSkinInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCargoEconomyInspectionOutput {
pub(crate) cargo_types_dir: String,
pub(crate) cargo_skin_pk4_path: String,
pub(crate) inspection: CargoEconomySourceReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCargoSelectorInspectionOutput {
pub(crate) cargo_types_dir: String,
pub(crate) cargo_skin_pk4_path: String,
pub(crate) selector: CargoSelectorReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeWinInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: WinInspectionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePk4ExtractionOutput {
pub(crate) path: String,
pub(crate) output_path: String,
pub(crate) extraction: Pk4ExtractionReport,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCampaignExeInspectionOutput {
pub(crate) path: String,
pub(crate) inspection: CampaignExeInspectionReport,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeProfileBlockExportDocument {
pub(crate) source_path: String,
pub(crate) profile_kind: String,
pub(crate) profile_family: String,
pub(crate) payload: Value,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeProfileBlockExportReport {
pub(crate) output_path: String,
pub(crate) profile_kind: String,
pub(crate) profile_family: String,
}

View file

@ -0,0 +1,2 @@
pub(super) mod inspect;
pub(super) mod state;

View file

@ -0,0 +1,72 @@
use rrt_fixtures::JsonDiffEntry;
use rrt_runtime::inspect::smp::save_load::{SmpLoadedSaveSlice, SmpSaveLoadSummary};
use rrt_runtime::summary::RuntimeSummary;
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Serialize)]
pub(crate) struct FinanceDiffEntry {
pub(crate) path: String,
pub(crate) left: Value,
pub(crate) right: Value,
}
#[derive(Debug, Serialize)]
pub(crate) struct FinanceDiffReport {
pub(crate) matches: bool,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<FinanceDiffEntry>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeFixtureSummaryReport {
pub(crate) fixture_id: String,
pub(crate) command_count: usize,
pub(crate) final_summary: RuntimeSummary,
pub(crate) expected_summary_matches: bool,
pub(crate) expected_summary_mismatches: Vec<String>,
pub(crate) expected_state_fragment_matches: bool,
pub(crate) expected_state_fragment_mismatches: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeStateSummaryReport {
pub(crate) snapshot_id: String,
pub(crate) summary: RuntimeSummary,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeStateDiffReport {
pub(crate) matches: bool,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<JsonDiffEntry>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSaveLoadSummaryOutput {
pub(crate) path: String,
pub(crate) summary: SmpSaveLoadSummary,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeLoadedSaveSliceOutput {
pub(crate) path: String,
pub(crate) save_slice: SmpLoadedSaveSlice,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSaveSliceExportOutput {
pub(crate) path: String,
pub(crate) output_path: String,
pub(crate) save_slice_id: String,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeOverlayImportExportOutput {
pub(crate) output_path: String,
pub(crate) import_id: String,
pub(crate) base_snapshot_path: String,
pub(crate) save_slice_path: String,
}

View file

@ -0,0 +1,472 @@
use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use rrt_runtime::inspect::smp::bundle::inspect_smp_file;
use serde::Serialize;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) semantic_family: String,
pub(crate) header_word_0_hex: String,
pub(crate) header_word_1_hex: String,
pub(crate) header_word_2_hex: String,
pub(crate) observed_entry_count: usize,
pub(crate) zero_trailer_entry_count: usize,
pub(crate) nonzero_trailer_entry_count: usize,
pub(crate) zero_trailer_entry_names: Vec<String>,
pub(crate) footer_progress_word_0_hex: String,
pub(crate) footer_progress_word_1_hex: String,
pub(crate) availability_by_name: BTreeMap<String, u32>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableEntrySample {
pub(crate) index: usize,
pub(crate) offset: usize,
pub(crate) text: String,
pub(crate) availability_dword: u32,
pub(crate) availability_dword_hex: String,
pub(crate) trailer_word: u32,
pub(crate) trailer_word_hex: String,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableInspectionReport {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) semantic_family: String,
pub(crate) header_word_0_hex: String,
pub(crate) header_word_1_hex: String,
pub(crate) header_word_2_hex: String,
pub(crate) observed_entry_capacity: usize,
pub(crate) observed_entry_count: usize,
pub(crate) zero_trailer_entry_count: usize,
pub(crate) nonzero_trailer_entry_count: usize,
pub(crate) zero_trailer_entry_names: Vec<String>,
pub(crate) entries: Vec<RuntimeCandidateTableEntrySample>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCandidateTableNamedRun {
pub(crate) prefix: String,
pub(crate) start_index: usize,
pub(crate) end_index: usize,
pub(crate) count: usize,
pub(crate) first_name: String,
pub(crate) last_name: String,
pub(crate) start_offset: usize,
pub(crate) end_offset: usize,
pub(crate) distinct_trailer_hex_words: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) common_profile_family: Option<String>,
pub(crate) common_semantic_family: Option<String>,
pub(crate) samples: Vec<RuntimeCandidateTableSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
pub(crate) fn compare_candidate_table(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_candidate_table_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let common_profile_family = samples
.first()
.map(|sample| sample.profile_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.profile_family == *family)
});
let common_semantic_family = samples
.first()
.map(|sample| sample.semantic_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.semantic_family == *family)
});
let differences = diff_candidate_table_samples(&samples)?;
let report = RuntimeCandidateTableComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
common_profile_family,
common_semantic_family,
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_candidate_table(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = load_candidate_table_inspection_report(smp_path)?;
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_candidate_table_sample(
smp_path: &Path,
) -> Result<RuntimeCandidateTableSample, Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let probe = inspection.rt3_105_save_name_table_probe.ok_or_else(|| {
format!(
"{} did not expose an RT3 1.05 candidate-availability table",
smp_path.display()
)
})?;
Ok(RuntimeCandidateTableSample {
path: smp_path.display().to_string(),
profile_family: probe.profile_family,
source_kind: probe.source_kind,
semantic_family: probe.semantic_family,
header_word_0_hex: probe.header_word_0_hex,
header_word_1_hex: probe.header_word_1_hex,
header_word_2_hex: probe.header_word_2_hex,
observed_entry_count: probe.observed_entry_count,
zero_trailer_entry_count: probe.zero_trailer_entry_count,
nonzero_trailer_entry_count: probe.nonzero_trailer_entry_count,
zero_trailer_entry_names: probe.zero_trailer_entry_names,
footer_progress_word_0_hex: probe.footer_progress_word_0_hex,
footer_progress_word_1_hex: probe.footer_progress_word_1_hex,
availability_by_name: probe
.entries
.into_iter()
.map(|entry| (entry.text, entry.availability_dword))
.collect(),
})
}
pub(crate) fn load_candidate_table_inspection_report(
smp_path: &Path,
) -> Result<RuntimeCandidateTableInspectionReport, Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
if let Some(probe) = inspection.rt3_105_save_name_table_probe {
return Ok(RuntimeCandidateTableInspectionReport {
path: smp_path.display().to_string(),
profile_family: probe.profile_family,
source_kind: probe.source_kind,
semantic_family: probe.semantic_family,
header_word_0_hex: probe.header_word_0_hex,
header_word_1_hex: probe.header_word_1_hex,
header_word_2_hex: probe.header_word_2_hex,
observed_entry_capacity: probe.observed_entry_capacity,
observed_entry_count: probe.observed_entry_count,
zero_trailer_entry_count: probe.zero_trailer_entry_count,
nonzero_trailer_entry_count: probe.nonzero_trailer_entry_count,
zero_trailer_entry_names: probe.zero_trailer_entry_names,
entries: probe
.entries
.into_iter()
.map(|entry| RuntimeCandidateTableEntrySample {
index: entry.index,
offset: entry.offset,
text: entry.text,
availability_dword: entry.availability_dword,
availability_dword_hex: entry.availability_dword_hex,
trailer_word: entry.trailer_word,
trailer_word_hex: entry.trailer_word_hex,
})
.collect(),
});
}
let bytes = fs::read(smp_path)?;
let header_offset = 0x6a70usize;
let entries_offset = 0x6ad1usize;
let block_end_offset = 0x73c0usize;
let entry_stride = 0x22usize;
if bytes.len() < block_end_offset
|| !matches_candidate_table_header_bytes(&bytes, header_offset)
{
return Err(format!(
"{} did not expose an RT3 1.05 candidate-availability table",
smp_path.display()
)
.into());
}
let observed_entry_capacity = read_u32_le(&bytes, header_offset + 0x1c)
.ok_or_else(|| format!("{} is missing candidate table capacity", smp_path.display()))?
as usize;
let observed_entry_count = read_u32_le(&bytes, header_offset + 0x20)
.ok_or_else(|| format!("{} is missing candidate table count", smp_path.display()))?
as usize;
if observed_entry_capacity < observed_entry_count {
return Err(format!(
"{} has invalid candidate table capacity/count {observed_entry_capacity}/{observed_entry_count}",
smp_path.display()
)
.into());
}
let entries_end_offset = entries_offset
.checked_add(
observed_entry_count
.checked_mul(entry_stride)
.ok_or("candidate table length overflow")?,
)
.ok_or("candidate table end overflow")?;
if entries_end_offset > block_end_offset {
return Err(format!(
"{} candidate table overruns fixed block end",
smp_path.display()
)
.into());
}
let mut zero_trailer_entry_names = Vec::new();
let mut entries = Vec::new();
for index in 0..observed_entry_count {
let offset = entries_offset + index * entry_stride;
let chunk = &bytes[offset..offset + entry_stride];
let nul_index = chunk
.iter()
.position(|byte| *byte == 0)
.unwrap_or(entry_stride - 4);
let text = std::str::from_utf8(&chunk[..nul_index]).map_err(|_| {
format!(
"{} contains invalid UTF-8 in candidate table",
smp_path.display()
)
})?;
let availability_dword =
read_u32_le(&bytes, offset + entry_stride - 4).ok_or_else(|| {
format!(
"{} is missing candidate availability dword",
smp_path.display()
)
})?;
if availability_dword == 0 {
zero_trailer_entry_names.push(text.to_string());
}
entries.push(RuntimeCandidateTableEntrySample {
index,
offset,
text: text.to_string(),
availability_dword,
availability_dword_hex: format!("0x{availability_dword:08x}"),
trailer_word: availability_dword,
trailer_word_hex: format!("0x{availability_dword:08x}"),
});
}
Ok(RuntimeCandidateTableInspectionReport {
path: smp_path.display().to_string(),
profile_family: classify_candidate_table_header_profile(
smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase()),
&bytes,
),
source_kind: match smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.as_deref()
{
Some("gmp") => "map-fixed-catalog-range",
Some("gms") => "save-fixed-catalog-range",
_ => "fixed-catalog-range",
}
.to_string(),
semantic_family: "scenario-named-candidate-availability-table".to_string(),
header_word_0_hex: format!(
"0x{:08x}",
read_u32_le(&bytes, header_offset).ok_or("missing candidate header word 0")?
),
header_word_1_hex: format!(
"0x{:08x}",
read_u32_le(&bytes, header_offset + 4).ok_or("missing candidate header word 1")?
),
header_word_2_hex: format!(
"0x{:08x}",
read_u32_le(&bytes, header_offset + 8).ok_or("missing candidate header word 2")?
),
observed_entry_capacity,
observed_entry_count,
zero_trailer_entry_count: zero_trailer_entry_names.len(),
nonzero_trailer_entry_count: observed_entry_count - zero_trailer_entry_names.len(),
zero_trailer_entry_names,
entries,
})
}
pub(crate) fn diff_candidate_table_samples(
samples: &[RuntimeCandidateTableSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"profile_family": sample.profile_family,
"source_kind": sample.source_kind,
"semantic_family": sample.semantic_family,
"header_word_0_hex": sample.header_word_0_hex,
"header_word_1_hex": sample.header_word_1_hex,
"header_word_2_hex": sample.header_word_2_hex,
"observed_entry_count": sample.observed_entry_count,
"zero_trailer_entry_count": sample.zero_trailer_entry_count,
"nonzero_trailer_entry_count": sample.nonzero_trailer_entry_count,
"zero_trailer_entry_names": sample.zero_trailer_entry_names,
"footer_progress_word_0_hex": sample.footer_progress_word_0_hex,
"footer_progress_word_1_hex": sample.footer_progress_word_1_hex,
"availability_by_name": sample.availability_by_name,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}
pub(crate) fn collect_numbered_candidate_name_runs(
entries: &[RuntimeCandidateTableEntrySample],
prefix: &str,
) -> Vec<RuntimeCandidateTableNamedRun> {
let mut numbered_entries = entries
.iter()
.filter_map(|entry| {
parse_numbered_candidate_name(&entry.text, prefix).map(|number| (entry, number))
})
.collect::<Vec<_>>();
numbered_entries.sort_by_key(|(entry, number)| (entry.index, *number));
let mut runs = Vec::new();
let mut cursor = 0usize;
while cursor < numbered_entries.len() {
let (first_entry, first_number) = numbered_entries[cursor];
let mut last_entry = first_entry;
let mut last_number = first_number;
let mut distinct_trailer_hex_words = BTreeSet::from([first_entry.trailer_word_hex.clone()]);
let mut next = cursor + 1;
while next < numbered_entries.len() {
let (entry, number) = numbered_entries[next];
if entry.index != last_entry.index + 1 || number != last_number + 1 {
break;
}
distinct_trailer_hex_words.insert(entry.trailer_word_hex.clone());
last_entry = entry;
last_number = number;
next += 1;
}
runs.push(RuntimeCandidateTableNamedRun {
prefix: prefix.to_string(),
start_index: first_entry.index,
end_index: last_entry.index,
count: next - cursor,
first_name: first_entry.text.clone(),
last_name: last_entry.text.clone(),
start_offset: first_entry.offset,
end_offset: last_entry.offset,
distinct_trailer_hex_words: distinct_trailer_hex_words.into_iter().collect(),
});
cursor = next;
}
runs
}
pub(crate) fn parse_numbered_candidate_name(text: &str, prefix: &str) -> Option<usize> {
let digits = text.strip_prefix(prefix)?;
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
digits.parse().ok()
}
pub(crate) fn matches_candidate_table_header_bytes(bytes: &[u8], header_offset: usize) -> bool {
matches!(
(
read_u32_le(bytes, header_offset + 0x08),
read_u32_le(bytes, header_offset + 0x0c),
read_u32_le(bytes, header_offset + 0x10),
read_u32_le(bytes, header_offset + 0x14),
read_u32_le(bytes, header_offset + 0x18),
read_u32_le(bytes, header_offset + 0x1c),
read_u32_le(bytes, header_offset + 0x20),
read_u32_le(bytes, header_offset + 0x24),
read_u32_le(bytes, header_offset + 0x28),
),
(
Some(0x0000332e),
Some(0x00000001),
Some(0x00000022),
Some(0x00000002),
Some(0x00000002),
Some(68),
Some(67),
Some(0x00000000),
Some(0x00000001),
)
)
}
pub(crate) fn classify_candidate_table_header_profile(
extension: Option<String>,
bytes: &[u8],
) -> String {
let word_2 = read_u32_le(bytes, 8);
let word_3 = read_u32_le(bytes, 12);
let word_5 = read_u32_le(bytes, 20);
match (extension.as_deref().unwrap_or(""), word_2, word_3, word_5) {
("gmp", Some(0x00040001), Some(0x00028000), Some(0x00000771)) => {
"rt3-105-map-container-v1".to_string()
}
("gmp", Some(0x00040001), Some(0x00018000), Some(0x00000746)) => {
"rt3-105-scenario-map-container-v1".to_string()
}
("gmp", Some(0x0001c001), Some(0x00018000), Some(0x00000754)) => {
"rt3-105-alt-map-container-v1".to_string()
}
("gms", Some(0x00040001), Some(0x00028000), Some(0x00000771)) => {
"rt3-105-save-container-v1".to_string()
}
("gms", Some(0x00040001), Some(0x00018000), Some(0x00000746)) => {
"rt3-105-scenario-save-container-v1".to_string()
}
("gms", Some(0x0001c001), Some(0x00018000), Some(0x00000754)) => {
"rt3-105-alt-save-container-v1".to_string()
}
("gmp", _, _, _) => "map-fixed-catalog-container-unknown".to_string(),
("gms", _, _, _) => "save-fixed-catalog-container-unknown".to_string(),
_ => "fixed-catalog-container-unknown".to_string(),
}
}
pub(crate) fn read_u32_le(bytes: &[u8], offset: usize) -> Option<u32> {
let chunk = bytes.get(offset..offset + 4)?;
Some(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
}
pub(crate) fn read_u16_le(bytes: &[u8], offset: usize) -> Option<u16> {
let chunk = bytes.get(offset..offset + 2)?;
Some(u16::from_le_bytes([chunk[0], chunk[1]]))
}
pub(crate) fn hex_encode(bytes: &[u8]) -> String {
let mut text = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use std::fmt::Write as _;
let _ = write!(&mut text, "{byte:02x}");
}
text
}

View file

@ -0,0 +1,104 @@
use std::collections::BTreeSet;
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeClassicProfileDifferenceValue {
pub(crate) path: String,
pub(crate) value: Value,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeClassicProfileDifference {
pub(crate) field_path: String,
pub(crate) values: Vec<RuntimeClassicProfileDifferenceValue>,
}
pub(crate) fn collect_json_multi_differences(
path: &str,
labeled_values: &[(String, Value)],
differences: &mut Vec<RuntimeClassicProfileDifference>,
) {
if labeled_values.is_empty() {
return;
}
if labeled_values
.iter()
.all(|(_, value)| matches!(value, Value::Object(_)))
{
let mut keys = BTreeSet::new();
for (_, value) in labeled_values {
if let Value::Object(map) = value {
keys.extend(map.keys().cloned());
}
}
for key in keys {
let next_path = format!("{path}.{key}");
let nested = labeled_values
.iter()
.map(|(label, value)| {
let nested_value = match value {
Value::Object(map) => map.get(&key).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
(label.clone(), nested_value)
})
.collect::<Vec<_>>();
collect_json_multi_differences(&next_path, &nested, differences);
}
return;
}
if labeled_values
.iter()
.all(|(_, value)| matches!(value, Value::Array(_)))
{
let max_len = labeled_values
.iter()
.filter_map(|(_, value)| match value {
Value::Array(items) => Some(items.len()),
_ => None,
})
.max()
.unwrap_or(0);
for index in 0..max_len {
let next_path = format!("{path}[{index}]");
let nested = labeled_values
.iter()
.map(|(label, value)| {
let nested_value = match value {
Value::Array(items) => items.get(index).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
(label.clone(), nested_value)
})
.collect::<Vec<_>>();
collect_json_multi_differences(&next_path, &nested, differences);
}
return;
}
let first = &labeled_values[0].1;
if labeled_values
.iter()
.skip(1)
.all(|(_, value)| value == first)
{
return;
}
differences.push(RuntimeClassicProfileDifference {
field_path: path.to_string(),
values: labeled_values
.iter()
.map(|(label, value)| RuntimeClassicProfileDifferenceValue {
path: label.clone(),
value: value.clone(),
})
.collect(),
});
}

View file

@ -0,0 +1,43 @@
mod candidate_table;
mod common;
mod post_special;
mod profiles;
mod recipe_book;
mod region;
mod setup_payload;
pub(super) use candidate_table::{compare_candidate_table, inspect_candidate_table};
pub(super) use post_special::compare_post_special_conditions_scalars;
pub(super) use profiles::{compare_classic_profile, compare_rt3_105_profile};
pub(super) use recipe_book::compare_recipe_book_lines;
pub(super) use region::compare_region_fixed_row_runs;
pub(super) use setup_payload::{compare_setup_launch_payload, compare_setup_payload_core};
#[cfg(test)]
pub(crate) use candidate_table::{
RuntimeCandidateTableEntrySample, RuntimeCandidateTableSample, diff_candidate_table_samples,
};
pub(crate) use candidate_table::{
RuntimeCandidateTableNamedRun, classify_candidate_table_header_profile,
collect_numbered_candidate_name_runs, load_candidate_table_inspection_report,
matches_candidate_table_header_bytes, read_u32_le,
};
#[cfg(test)]
pub(crate) use profiles::{
RuntimeClassicProfileSample, RuntimeRt3105ProfileSample, diff_classic_profile_samples,
diff_rt3_105_profile_samples,
};
pub(crate) use recipe_book::{
RuntimeRecipeBookLineFieldSummary, build_recipe_line_field_summaries,
intersect_nonzero_recipe_line_paths, load_recipe_book_line_sample,
};
#[cfg(test)]
pub(crate) use recipe_book::{
RuntimeRecipeBookLineSample, diff_recipe_book_line_content_samples,
diff_recipe_book_line_samples,
};
#[cfg(test)]
pub(crate) use setup_payload::{
RuntimeSetupLaunchPayloadSample, RuntimeSetupPayloadCoreSample,
diff_setup_launch_payload_samples, diff_setup_payload_core_samples,
};

View file

@ -0,0 +1,94 @@
use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) nonzero_relative_offset_hexes: Vec<String>,
pub(crate) values_by_relative_offset_hex: BTreeMap<String, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) common_profile_family: Option<String>,
pub(crate) samples: Vec<RuntimePostSpecialConditionsScalarSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
pub(crate) fn compare_post_special_conditions_scalars(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_post_special_conditions_scalar_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let common_profile_family = samples
.first()
.map(|sample| sample.profile_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.profile_family == *family)
});
let differences = diff_post_special_conditions_scalar_samples(&samples)?;
let report = RuntimePostSpecialConditionsScalarComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
common_profile_family,
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_post_special_conditions_scalar_sample(
smp_path: &Path,
) -> Result<RuntimePostSpecialConditionsScalarSample, Box<dyn std::error::Error>> {
let sample =
crate::app::runtime_scan::post_special::load_post_special_conditions_scalar_scan_sample(
smp_path,
)?;
Ok(RuntimePostSpecialConditionsScalarSample {
path: sample.path,
profile_family: sample.profile_family,
source_kind: sample.source_kind,
nonzero_relative_offset_hexes: sample
.nonzero_relative_offsets
.into_iter()
.map(|offset| format!("0x{offset:x}"))
.collect(),
values_by_relative_offset_hex: sample.values_by_relative_offset_hex,
})
}
pub(crate) fn diff_post_special_conditions_scalar_samples(
samples: &[RuntimePostSpecialConditionsScalarSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"profile_family": sample.profile_family,
"source_kind": sample.source_kind,
"nonzero_relative_offset_hexes": sample.nonzero_relative_offset_hexes,
"values_by_relative_offset_hex": sample.values_by_relative_offset_hex,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}

View file

@ -0,0 +1,195 @@
use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::path::{Path, PathBuf};
use rrt_runtime::inspect::smp::{
bundle::inspect_smp_file,
profiles::{SmpClassicPackedProfileBlock, SmpRt3105PackedProfileBlock},
};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeClassicProfileSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) progress_32dc_offset: usize,
pub(crate) progress_3714_offset: usize,
pub(crate) progress_3715_offset: usize,
pub(crate) packed_profile_offset: usize,
pub(crate) packed_profile_len: usize,
pub(crate) packed_profile_block: SmpClassicPackedProfileBlock,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeClassicProfileComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) common_profile_family: Option<String>,
pub(crate) samples: Vec<RuntimeClassicProfileSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeRt3105ProfileSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) packed_profile_offset: usize,
pub(crate) packed_profile_len: usize,
pub(crate) packed_profile_block: SmpRt3105PackedProfileBlock,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRt3105ProfileComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) common_profile_family: Option<String>,
pub(crate) samples: Vec<RuntimeRt3105ProfileSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
pub(crate) fn compare_classic_profile(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_classic_profile_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let common_profile_family = samples
.first()
.map(|sample| sample.profile_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.profile_family == *family)
});
let differences = diff_classic_profile_samples(&samples)?;
let report = RuntimeClassicProfileComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
common_profile_family,
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn compare_rt3_105_profile(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_rt3_105_profile_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let common_profile_family = samples
.first()
.map(|sample| sample.profile_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.profile_family == *family)
});
let differences = diff_rt3_105_profile_samples(&samples)?;
let report = RuntimeRt3105ProfileComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
common_profile_family,
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_classic_profile_sample(
smp_path: &Path,
) -> Result<RuntimeClassicProfileSample, Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let probe = inspection.classic_rehydrate_profile_probe.ok_or_else(|| {
format!(
"{} did not expose a classic rehydrate packed-profile block",
smp_path.display()
)
})?;
Ok(RuntimeClassicProfileSample {
path: smp_path.display().to_string(),
profile_family: probe.profile_family,
progress_32dc_offset: probe.progress_32dc_offset,
progress_3714_offset: probe.progress_3714_offset,
progress_3715_offset: probe.progress_3715_offset,
packed_profile_offset: probe.packed_profile_offset,
packed_profile_len: probe.packed_profile_len,
packed_profile_block: probe.packed_profile_block,
})
}
pub(crate) fn load_rt3_105_profile_sample(
smp_path: &Path,
) -> Result<RuntimeRt3105ProfileSample, Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let probe = inspection.rt3_105_packed_profile_probe.ok_or_else(|| {
format!(
"{} did not expose an RT3 1.05 packed-profile block",
smp_path.display()
)
})?;
Ok(RuntimeRt3105ProfileSample {
path: smp_path.display().to_string(),
profile_family: probe.profile_family,
packed_profile_offset: probe.packed_profile_offset,
packed_profile_len: probe.packed_profile_len,
packed_profile_block: probe.packed_profile_block,
})
}
pub(crate) fn diff_classic_profile_samples(
samples: &[RuntimeClassicProfileSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"profile_family": sample.profile_family,
"progress_32dc_offset": sample.progress_32dc_offset,
"progress_3714_offset": sample.progress_3714_offset,
"progress_3715_offset": sample.progress_3715_offset,
"packed_profile_offset": sample.packed_profile_offset,
"packed_profile_len": sample.packed_profile_len,
"packed_profile_block": sample.packed_profile_block,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}
pub(crate) fn diff_rt3_105_profile_samples(
samples: &[RuntimeRt3105ProfileSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"profile_family": sample.profile_family,
"packed_profile_offset": sample.packed_profile_offset,
"packed_profile_len": sample.packed_profile_len,
"packed_profile_block": sample.packed_profile_block,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}

View file

@ -0,0 +1,250 @@
use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use rrt_runtime::inspect::smp::bundle::inspect_smp_file;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeRecipeBookLineSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) book_count: usize,
pub(crate) book_stride_hex: String,
pub(crate) line_count: usize,
pub(crate) line_stride_hex: String,
pub(crate) book_head_kind_by_index: BTreeMap<String, String>,
pub(crate) book_line_area_kind_by_index: BTreeMap<String, String>,
pub(crate) max_annual_production_word_hex_by_book: BTreeMap<String, String>,
pub(crate) line_kind_by_path: BTreeMap<String, String>,
pub(crate) mode_word_hex_by_path: BTreeMap<String, String>,
pub(crate) annual_amount_word_hex_by_path: BTreeMap<String, String>,
pub(crate) supplied_cargo_token_word_hex_by_path: BTreeMap<String, String>,
pub(crate) demanded_cargo_token_word_hex_by_path: BTreeMap<String, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRecipeBookLineComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) content_matches: bool,
pub(crate) common_profile_family: Option<String>,
pub(crate) samples: Vec<RuntimeRecipeBookLineSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
pub(crate) content_difference_count: usize,
pub(crate) content_differences: Vec<RuntimeClassicProfileDifference>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRecipeBookLineFieldSummary {
pub(crate) line_path: String,
pub(crate) file_count_present: usize,
pub(crate) distinct_value_count: usize,
pub(crate) sample_value_hexes: Vec<String>,
}
pub(crate) fn compare_recipe_book_lines(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_recipe_book_line_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let common_profile_family = samples
.first()
.map(|sample| sample.profile_family.clone())
.filter(|family| {
samples
.iter()
.all(|sample| sample.profile_family == *family)
});
let differences = diff_recipe_book_line_samples(&samples)?;
let content_differences = diff_recipe_book_line_content_samples(&samples)?;
let report = RuntimeRecipeBookLineComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
content_matches: content_differences.is_empty(),
common_profile_family,
difference_count: differences.len(),
differences,
content_difference_count: content_differences.len(),
content_differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_recipe_book_line_sample(
smp_path: &Path,
) -> Result<RuntimeRecipeBookLineSample, Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let probe = inspection.recipe_book_summary_probe.ok_or_else(|| {
format!(
"{} did not expose a grounded recipe-book summary block",
smp_path.display()
)
})?;
let mut book_head_kind_by_index = BTreeMap::new();
let mut book_line_area_kind_by_index = BTreeMap::new();
let mut max_annual_production_word_hex_by_book = BTreeMap::new();
let mut line_kind_by_path = BTreeMap::new();
let mut mode_word_hex_by_path = BTreeMap::new();
let mut annual_amount_word_hex_by_path = BTreeMap::new();
let mut supplied_cargo_token_word_hex_by_path = BTreeMap::new();
let mut demanded_cargo_token_word_hex_by_path = BTreeMap::new();
for book in &probe.books {
let book_key = format!("book{:02}", book.book_index);
book_head_kind_by_index.insert(book_key.clone(), book.head_kind.clone());
book_line_area_kind_by_index.insert(book_key.clone(), book.line_area_kind.clone());
max_annual_production_word_hex_by_book.insert(
book_key.clone(),
book.max_annual_production_word_hex.clone(),
);
for line in &book.lines {
let line_key = format!("{book_key}.line{:02}", line.line_index);
line_kind_by_path.insert(line_key.clone(), line.line_kind.clone());
mode_word_hex_by_path.insert(line_key.clone(), line.mode_word_hex.clone());
annual_amount_word_hex_by_path
.insert(line_key.clone(), line.annual_amount_word_hex.clone());
supplied_cargo_token_word_hex_by_path
.insert(line_key.clone(), line.supplied_cargo_token_word_hex.clone());
demanded_cargo_token_word_hex_by_path
.insert(line_key.clone(), line.demanded_cargo_token_word_hex.clone());
}
}
Ok(RuntimeRecipeBookLineSample {
path: smp_path.display().to_string(),
profile_family: probe.profile_family,
source_kind: probe.source_kind,
book_count: probe.book_count,
book_stride_hex: probe.book_stride_hex,
line_count: probe.line_count,
line_stride_hex: probe.line_stride_hex,
book_head_kind_by_index,
book_line_area_kind_by_index,
max_annual_production_word_hex_by_book,
line_kind_by_path,
mode_word_hex_by_path,
annual_amount_word_hex_by_path,
supplied_cargo_token_word_hex_by_path,
demanded_cargo_token_word_hex_by_path,
})
}
pub(crate) fn diff_recipe_book_line_samples(
samples: &[RuntimeRecipeBookLineSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"profile_family": sample.profile_family,
"source_kind": sample.source_kind,
"book_count": sample.book_count,
"book_stride_hex": sample.book_stride_hex,
"line_count": sample.line_count,
"line_stride_hex": sample.line_stride_hex,
"book_head_kind_by_index": sample.book_head_kind_by_index,
"book_line_area_kind_by_index": sample.book_line_area_kind_by_index,
"max_annual_production_word_hex_by_book": sample.max_annual_production_word_hex_by_book,
"line_kind_by_path": sample.line_kind_by_path,
"mode_word_hex_by_path": sample.mode_word_hex_by_path,
"annual_amount_word_hex_by_path": sample.annual_amount_word_hex_by_path,
"supplied_cargo_token_word_hex_by_path": sample.supplied_cargo_token_word_hex_by_path,
"demanded_cargo_token_word_hex_by_path": sample.demanded_cargo_token_word_hex_by_path,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}
pub(crate) fn diff_recipe_book_line_content_samples(
samples: &[RuntimeRecipeBookLineSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"book_count": sample.book_count,
"book_stride_hex": sample.book_stride_hex,
"line_count": sample.line_count,
"line_stride_hex": sample.line_stride_hex,
"book_head_kind_by_index": sample.book_head_kind_by_index,
"book_line_area_kind_by_index": sample.book_line_area_kind_by_index,
"max_annual_production_word_hex_by_book": sample.max_annual_production_word_hex_by_book,
"line_kind_by_path": sample.line_kind_by_path,
"mode_word_hex_by_path": sample.mode_word_hex_by_path,
"annual_amount_word_hex_by_path": sample.annual_amount_word_hex_by_path,
"supplied_cargo_token_word_hex_by_path": sample.supplied_cargo_token_word_hex_by_path,
"demanded_cargo_token_word_hex_by_path": sample.demanded_cargo_token_word_hex_by_path,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}
pub(crate) fn intersect_nonzero_recipe_line_paths<'a>(
maps: impl Iterator<Item = &'a BTreeMap<String, String>>,
) -> Vec<String> {
let mut maps = maps.peekable();
if maps.peek().is_none() {
return Vec::new();
}
let mut stable = maps
.next()
.map(|map| map.keys().cloned().collect::<BTreeSet<_>>())
.unwrap_or_default();
for map in maps {
let current = map.keys().cloned().collect::<BTreeSet<_>>();
stable = stable.intersection(&current).cloned().collect();
}
stable.into_iter().collect()
}
pub(crate) fn build_recipe_line_field_summaries<'a>(
maps: impl Iterator<Item = &'a BTreeMap<String, String>>,
) -> Vec<RuntimeRecipeBookLineFieldSummary> {
let mut value_sets = BTreeMap::<String, BTreeSet<String>>::new();
let mut counts = BTreeMap::<String, usize>::new();
for map in maps {
for (line_path, value_hex) in map {
*counts.entry(line_path.clone()).or_default() += 1;
value_sets
.entry(line_path.clone())
.or_default()
.insert(value_hex.clone());
}
}
counts
.into_iter()
.map(
|(line_path, file_count_present)| RuntimeRecipeBookLineFieldSummary {
line_path: line_path.clone(),
file_count_present,
distinct_value_count: value_sets.get(&line_path).map(BTreeSet::len).unwrap_or(0),
sample_value_hexes: value_sets
.get(&line_path)
.map(|values| values.iter().take(8).cloned().collect())
.unwrap_or_default(),
},
)
.collect()
}

View file

@ -0,0 +1,33 @@
use std::path::Path;
use rrt_runtime::inspect::smp::{
regions::{
SmpSaveRegionFixedRowRunComparisonReport, compare_save_region_fixed_row_run_candidates,
},
world::inspect_save_company_and_chairman_analysis_file,
};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRegionFixedRowRunComparisonOutput {
pub(crate) left_path: String,
pub(crate) right_path: String,
pub(crate) comparison: SmpSaveRegionFixedRowRunComparisonReport,
}
pub(crate) fn compare_region_fixed_row_runs(
left_path: &Path,
right_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let left = inspect_save_company_and_chairman_analysis_file(left_path)?;
let right = inspect_save_company_and_chairman_analysis_file(right_path)?;
let comparison = compare_save_region_fixed_row_run_candidates(&left, &right)
.ok_or("save inspection did not expose grounded region fixed-row candidate probes")?;
let report = RuntimeRegionFixedRowRunComparisonOutput {
left_path: left_path.display().to_string(),
right_path: right_path.display().to_string(),
comparison,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}

View file

@ -0,0 +1,345 @@
use super::candidate_table::{
classify_candidate_table_header_profile, hex_encode, read_u16_le, read_u32_le,
};
use super::common::{RuntimeClassicProfileDifference, collect_json_multi_differences};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use rrt_runtime::inspect::campaign::{CAMPAIGN_SCENARIO_COUNT, OBSERVED_CAMPAIGN_SCENARIO_NAMES};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupPayloadCoreSample {
pub(crate) path: String,
pub(crate) file_extension: String,
pub(crate) inferred_profile_family: String,
pub(crate) payload_word_0x14: u16,
pub(crate) payload_word_0x14_hex: String,
pub(crate) payload_byte_0x20: u8,
pub(crate) payload_byte_0x20_hex: String,
pub(crate) marker_bytes_0x2c9_0x2d0_hex: String,
pub(crate) row_category_byte_0x31a: u8,
pub(crate) row_category_byte_0x31a_hex: String,
pub(crate) row_visibility_byte_0x31b: u8,
pub(crate) row_visibility_byte_0x31b_hex: String,
pub(crate) row_visibility_byte_0x31c: u8,
pub(crate) row_visibility_byte_0x31c_hex: String,
pub(crate) row_count_word_0x3ae: u16,
pub(crate) row_count_word_0x3ae_hex: String,
pub(crate) payload_word_0x3b2: u16,
pub(crate) payload_word_0x3b2_hex: String,
pub(crate) payload_word_0x3ba: u16,
pub(crate) payload_word_0x3ba_hex: String,
pub(crate) candidate_header_word_0_hex: Option<String>,
pub(crate) candidate_header_word_1_hex: Option<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupPayloadCoreComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) samples: Vec<RuntimeSetupPayloadCoreSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupLaunchPayloadSample {
pub(crate) path: String,
pub(crate) file_extension: String,
pub(crate) inferred_profile_family: String,
pub(crate) launch_flag_byte_0x22: u8,
pub(crate) launch_flag_byte_0x22_hex: String,
pub(crate) campaign_progress_in_known_range: bool,
pub(crate) campaign_progress_scenario_name: Option<String>,
pub(crate) campaign_progress_page_index: Option<usize>,
pub(crate) launch_selector_byte_0x33: u8,
pub(crate) launch_selector_byte_0x33_hex: String,
pub(crate) launch_token_block_0x23_0x32_hex: String,
pub(crate) campaign_selector_values: BTreeMap<String, u8>,
pub(crate) nonzero_campaign_selector_values: BTreeMap<String, u8>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSetupLaunchPayloadComparisonReport {
pub(crate) file_count: usize,
pub(crate) matches: bool,
pub(crate) samples: Vec<RuntimeSetupLaunchPayloadSample>,
pub(crate) difference_count: usize,
pub(crate) differences: Vec<RuntimeClassicProfileDifference>,
}
pub(crate) fn compare_setup_payload_core(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_setup_payload_core_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let differences = diff_setup_payload_core_samples(&samples)?;
let report = RuntimeSetupPayloadCoreComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn compare_setup_launch_payload(
smp_paths: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
let samples = smp_paths
.iter()
.map(|path| load_setup_launch_payload_sample(path))
.collect::<Result<Vec<_>, _>>()?;
let differences = diff_setup_launch_payload_samples(&samples)?;
let report = RuntimeSetupLaunchPayloadComparisonReport {
file_count: samples.len(),
matches: differences.is_empty(),
difference_count: differences.len(),
differences,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_setup_payload_core_sample(
smp_path: &Path,
) -> Result<RuntimeSetupPayloadCoreSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let inferred_profile_family =
classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let candidate_header_word_0 = read_u32_le(&bytes, 0x6a70);
let candidate_header_word_1 = read_u32_le(&bytes, 0x6a74);
Ok(RuntimeSetupPayloadCoreSample {
path: smp_path.display().to_string(),
file_extension: extension,
inferred_profile_family,
payload_word_0x14: read_u16_le(&bytes, 0x14)
.ok_or_else(|| format!("{} missing setup payload word +0x14", smp_path.display()))?,
payload_word_0x14_hex: format!(
"0x{:04x}",
read_u16_le(&bytes, 0x14).ok_or_else(|| format!(
"{} missing setup payload word +0x14",
smp_path.display()
))?
),
payload_byte_0x20: bytes
.get(0x20)
.copied()
.ok_or_else(|| format!("{} missing setup payload byte +0x20", smp_path.display()))?,
payload_byte_0x20_hex: format!(
"0x{:02x}",
bytes.get(0x20).copied().ok_or_else(|| format!(
"{} missing setup payload byte +0x20",
smp_path.display()
))?
),
marker_bytes_0x2c9_0x2d0_hex: bytes
.get(0x2c9..0x2d1)
.map(hex_encode)
.ok_or_else(|| format!("{} missing setup payload marker bytes", smp_path.display()))?,
row_category_byte_0x31a: bytes
.get(0x31a)
.copied()
.ok_or_else(|| format!("{} missing setup payload byte +0x31a", smp_path.display()))?,
row_category_byte_0x31a_hex: format!(
"0x{:02x}",
bytes.get(0x31a).copied().ok_or_else(|| format!(
"{} missing setup payload byte +0x31a",
smp_path.display()
))?
),
row_visibility_byte_0x31b: bytes
.get(0x31b)
.copied()
.ok_or_else(|| format!("{} missing setup payload byte +0x31b", smp_path.display()))?,
row_visibility_byte_0x31b_hex: format!(
"0x{:02x}",
bytes.get(0x31b).copied().ok_or_else(|| format!(
"{} missing setup payload byte +0x31b",
smp_path.display()
))?
),
row_visibility_byte_0x31c: bytes
.get(0x31c)
.copied()
.ok_or_else(|| format!("{} missing setup payload byte +0x31c", smp_path.display()))?,
row_visibility_byte_0x31c_hex: format!(
"0x{:02x}",
bytes.get(0x31c).copied().ok_or_else(|| format!(
"{} missing setup payload byte +0x31c",
smp_path.display()
))?
),
row_count_word_0x3ae: read_u16_le(&bytes, 0x3ae)
.ok_or_else(|| format!("{} missing setup payload word +0x3ae", smp_path.display()))?,
row_count_word_0x3ae_hex: format!(
"0x{:04x}",
read_u16_le(&bytes, 0x3ae).ok_or_else(|| format!(
"{} missing setup payload word +0x3ae",
smp_path.display()
))?
),
payload_word_0x3b2: read_u16_le(&bytes, 0x3b2)
.ok_or_else(|| format!("{} missing setup payload word +0x3b2", smp_path.display()))?,
payload_word_0x3b2_hex: format!(
"0x{:04x}",
read_u16_le(&bytes, 0x3b2).ok_or_else(|| format!(
"{} missing setup payload word +0x3b2",
smp_path.display()
))?
),
payload_word_0x3ba: read_u16_le(&bytes, 0x3ba)
.ok_or_else(|| format!("{} missing setup payload word +0x3ba", smp_path.display()))?,
payload_word_0x3ba_hex: format!(
"0x{:04x}",
read_u16_le(&bytes, 0x3ba).ok_or_else(|| format!(
"{} missing setup payload word +0x3ba",
smp_path.display()
))?
),
candidate_header_word_0_hex: candidate_header_word_0.map(|value| format!("0x{value:08x}")),
candidate_header_word_1_hex: candidate_header_word_1.map(|value| format!("0x{value:08x}")),
})
}
pub(crate) fn load_setup_launch_payload_sample(
smp_path: &Path,
) -> Result<RuntimeSetupLaunchPayloadSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let inferred_profile_family =
classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let launch_flag_byte_0x22 = bytes
.get(0x22)
.copied()
.ok_or_else(|| format!("{} missing setup launch byte +0x22", smp_path.display()))?;
let launch_selector_byte_0x33 = bytes
.get(0x33)
.copied()
.ok_or_else(|| format!("{} missing setup launch byte +0x33", smp_path.display()))?;
let token_block = bytes
.get(0x23..0x33)
.ok_or_else(|| format!("{} missing setup launch token block", smp_path.display()))?;
let campaign_progress_in_known_range =
(launch_flag_byte_0x22 as usize) < CAMPAIGN_SCENARIO_COUNT;
let campaign_progress_scenario_name = campaign_progress_in_known_range
.then(|| OBSERVED_CAMPAIGN_SCENARIO_NAMES[launch_flag_byte_0x22 as usize].to_string());
let campaign_progress_page_index = match launch_flag_byte_0x22 {
0..=4 => Some(1),
5..=9 => Some(2),
10..=12 => Some(3),
13..=15 => Some(4),
_ => None,
};
let campaign_selector_values = OBSERVED_CAMPAIGN_SCENARIO_NAMES
.iter()
.enumerate()
.map(|(index, name)| (name.to_string(), token_block[index]))
.collect::<BTreeMap<_, _>>();
let nonzero_campaign_selector_values = campaign_selector_values
.iter()
.filter_map(|(name, value)| (*value != 0).then_some((name.clone(), *value)))
.collect::<BTreeMap<_, _>>();
Ok(RuntimeSetupLaunchPayloadSample {
path: smp_path.display().to_string(),
file_extension: extension,
inferred_profile_family,
launch_flag_byte_0x22,
launch_flag_byte_0x22_hex: format!("0x{launch_flag_byte_0x22:02x}"),
campaign_progress_in_known_range,
campaign_progress_scenario_name,
campaign_progress_page_index,
launch_selector_byte_0x33,
launch_selector_byte_0x33_hex: format!("0x{launch_selector_byte_0x33:02x}"),
launch_token_block_0x23_0x32_hex: hex_encode(token_block),
campaign_selector_values,
nonzero_campaign_selector_values,
})
}
pub(crate) fn diff_setup_payload_core_samples(
samples: &[RuntimeSetupPayloadCoreSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"file_extension": sample.file_extension,
"inferred_profile_family": sample.inferred_profile_family,
"payload_word_0x14": sample.payload_word_0x14,
"payload_word_0x14_hex": sample.payload_word_0x14_hex,
"payload_byte_0x20": sample.payload_byte_0x20,
"payload_byte_0x20_hex": sample.payload_byte_0x20_hex,
"marker_bytes_0x2c9_0x2d0_hex": sample.marker_bytes_0x2c9_0x2d0_hex,
"row_category_byte_0x31a": sample.row_category_byte_0x31a,
"row_category_byte_0x31a_hex": sample.row_category_byte_0x31a_hex,
"row_visibility_byte_0x31b": sample.row_visibility_byte_0x31b,
"row_visibility_byte_0x31b_hex": sample.row_visibility_byte_0x31b_hex,
"row_visibility_byte_0x31c": sample.row_visibility_byte_0x31c,
"row_visibility_byte_0x31c_hex": sample.row_visibility_byte_0x31c_hex,
"row_count_word_0x3ae": sample.row_count_word_0x3ae,
"row_count_word_0x3ae_hex": sample.row_count_word_0x3ae_hex,
"payload_word_0x3b2": sample.payload_word_0x3b2,
"payload_word_0x3b2_hex": sample.payload_word_0x3b2_hex,
"payload_word_0x3ba": sample.payload_word_0x3ba,
"payload_word_0x3ba_hex": sample.payload_word_0x3ba_hex,
"candidate_header_word_0_hex": sample.candidate_header_word_0_hex,
"candidate_header_word_1_hex": sample.candidate_header_word_1_hex,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}
pub(crate) fn diff_setup_launch_payload_samples(
samples: &[RuntimeSetupLaunchPayloadSample],
) -> Result<Vec<RuntimeClassicProfileDifference>, Box<dyn std::error::Error>> {
let labeled_values = samples
.iter()
.map(|sample| {
(
sample.path.clone(),
serde_json::json!({
"file_extension": sample.file_extension,
"inferred_profile_family": sample.inferred_profile_family,
"launch_flag_byte_0x22": sample.launch_flag_byte_0x22,
"launch_flag_byte_0x22_hex": sample.launch_flag_byte_0x22_hex,
"campaign_progress_in_known_range": sample.campaign_progress_in_known_range,
"campaign_progress_scenario_name": sample.campaign_progress_scenario_name,
"campaign_progress_page_index": sample.campaign_progress_page_index,
"launch_selector_byte_0x33": sample.launch_selector_byte_0x33,
"launch_selector_byte_0x33_hex": sample.launch_selector_byte_0x33_hex,
"launch_token_block_0x23_0x32_hex": sample.launch_token_block_0x23_0x32_hex,
"campaign_selector_values": sample.campaign_selector_values,
"nonzero_campaign_selector_values": sample.nonzero_campaign_selector_values,
}),
)
})
.collect::<Vec<_>>();
let mut differences = Vec::new();
collect_json_multi_differences("$", &labeled_values, &mut differences);
Ok(differences)
}

View file

@ -0,0 +1,115 @@
use std::path::Path;
use crate::app::helpers::state_io::print_runtime_validation_report;
use crate::app::reports::state::{RuntimeFixtureSummaryReport, RuntimeStateSummaryReport};
use rrt_fixtures::{
normalize_runtime_state,
summary::compare_expected_state_fragment,
validation::{load_fixture_document, validate_fixture_document},
};
use rrt_runtime::{
engine::execute_step_command,
persistence::{
RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION,
save_runtime_snapshot_document,
},
summary::RuntimeSummary,
};
pub(crate) fn validate_fixture(fixture_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let fixture = load_fixture_document(fixture_path)?;
let report = validate_fixture_document(&fixture);
print_runtime_validation_report(&report)?;
if !report.valid {
return Err(format!("fixture validation failed for {}", fixture_path.display()).into());
}
Ok(())
}
pub(crate) fn summarize_fixture(fixture_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let fixture = load_fixture_document(fixture_path)?;
let validation_report = validate_fixture_document(&fixture);
if !validation_report.valid {
print_runtime_validation_report(&validation_report)?;
return Err(format!("fixture validation failed for {}", fixture_path.display()).into());
}
let mut state = fixture.state.clone();
for command in &fixture.commands {
execute_step_command(&mut state, command)?;
}
let final_summary = RuntimeSummary::from_state(&state);
let expected_summary_mismatches = fixture.expected_summary.compare(&final_summary);
let expected_state_fragment_mismatches = match &fixture.expected_state_fragment {
Some(expected_fragment) => {
let normalized_state = normalize_runtime_state(&state)?;
compare_expected_state_fragment(expected_fragment, &normalized_state)
}
None => Vec::new(),
};
let report = RuntimeFixtureSummaryReport {
fixture_id: fixture.fixture_id,
command_count: fixture.commands.len(),
expected_summary_matches: expected_summary_mismatches.is_empty(),
expected_summary_mismatches: expected_summary_mismatches.clone(),
expected_state_fragment_matches: expected_state_fragment_mismatches.is_empty(),
expected_state_fragment_mismatches: expected_state_fragment_mismatches.clone(),
final_summary,
};
println!("{}", serde_json::to_string_pretty(&report)?);
if !expected_summary_mismatches.is_empty() || !expected_state_fragment_mismatches.is_empty() {
let mut mismatch_messages = expected_summary_mismatches;
mismatch_messages.extend(expected_state_fragment_mismatches);
return Err(format!(
"fixture summary mismatched expected output: {}",
mismatch_messages.join("; ")
)
.into());
}
Ok(())
}
pub(crate) fn export_fixture_state(
fixture_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let fixture = load_fixture_document(fixture_path)?;
let validation_report = validate_fixture_document(&fixture);
if !validation_report.valid {
print_runtime_validation_report(&validation_report)?;
return Err(format!("fixture validation failed for {}", fixture_path.display()).into());
}
let mut state = fixture.state.clone();
for command in &fixture.commands {
execute_step_command(&mut state, command)?;
}
let snapshot = RuntimeSnapshotDocument {
format_version: SNAPSHOT_FORMAT_VERSION,
snapshot_id: format!("{}-final-state", fixture.fixture_id),
source: RuntimeSnapshotSource {
source_fixture_id: Some(fixture.fixture_id.clone()),
description: Some(format!(
"Exported final runtime state for fixture {}",
fixture.fixture_id
)),
},
state,
};
save_runtime_snapshot_document(output_path, &snapshot)?;
let summary = snapshot.summary();
println!(
"{}",
serde_json::to_string_pretty(&RuntimeStateSummaryReport {
snapshot_id: snapshot.snapshot_id,
summary,
})?
);
Ok(())
}

View file

@ -0,0 +1,9 @@
mod fixtures;
mod save_import;
mod save_load;
mod state;
pub(crate) use fixtures::{export_fixture_state, summarize_fixture, validate_fixture};
pub(crate) use save_import::{export_overlay_import, export_save_slice, snapshot_save_state};
pub(crate) use save_load::{load_save_slice, summarize_save_load};
pub(crate) use state::{diff_state, snapshot_state, summarize_state};

View file

@ -0,0 +1,70 @@
use std::path::Path;
use crate::app::helpers::state_io::{
export_runtime_overlay_import_document, export_runtime_save_slice_document,
};
use crate::app::reports::state::RuntimeStateSummaryReport;
use rrt_runtime::{
documents::build_runtime_state_input_from_save_slice,
inspect::smp::save_load::load_save_slice_file,
persistence::{
RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION,
save_runtime_snapshot_document,
},
};
pub(crate) fn snapshot_save_state(
smp_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let save_slice = load_save_slice_file(smp_path)?;
let input = build_runtime_state_input_from_save_slice(
&save_slice,
smp_path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("save-state"),
Some(format!(
"Projected partial runtime state from save {}",
smp_path.display()
)),
)
.map_err(|err| format!("failed to project save slice: {err}"))?;
let snapshot = RuntimeSnapshotDocument {
format_version: SNAPSHOT_FORMAT_VERSION,
snapshot_id: format!("{}-snapshot", input.input_id),
source: RuntimeSnapshotSource {
source_fixture_id: None,
description: input.description,
},
state: input.state,
};
save_runtime_snapshot_document(output_path, &snapshot)?;
let report = RuntimeStateSummaryReport {
snapshot_id: snapshot.snapshot_id.clone(),
summary: snapshot.summary(),
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn export_save_slice(
smp_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let save_slice = load_save_slice_file(smp_path)?;
let report = export_runtime_save_slice_document(smp_path, output_path, save_slice)?;
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn export_overlay_import(
snapshot_path: &Path,
save_slice_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report =
export_runtime_overlay_import_document(snapshot_path, save_slice_path, output_path)?;
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}

View file

@ -0,0 +1,29 @@
use std::path::Path;
use crate::app::reports::state::{RuntimeLoadedSaveSliceOutput, RuntimeSaveLoadSummaryOutput};
use rrt_runtime::inspect::smp::{bundle::inspect_smp_file, save_load::load_save_slice_file};
pub(crate) fn summarize_save_load(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let summary = inspection.save_load_summary.ok_or_else(|| {
format!(
"{} did not expose a recognizable save-load summary",
smp_path.display()
)
})?;
let report = RuntimeSaveLoadSummaryOutput {
path: smp_path.display().to_string(),
summary,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_save_slice(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeLoadedSaveSliceOutput {
path: smp_path.display().to_string(),
save_slice: load_save_slice_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}

View file

@ -0,0 +1,81 @@
use std::path::Path;
use crate::app::helpers::state_io::load_normalized_runtime_state;
use crate::app::reports::state::{RuntimeStateDiffReport, RuntimeStateSummaryReport};
use rrt_fixtures::diff_json_values;
use rrt_runtime::{
documents::load_runtime_state_input,
persistence::{
RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION,
load_runtime_snapshot_document, save_runtime_snapshot_document,
validate_runtime_snapshot_document,
},
summary::RuntimeSummary,
};
pub(crate) fn summarize_state(snapshot_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
if let Ok(snapshot) = load_runtime_snapshot_document(snapshot_path) {
validate_runtime_snapshot_document(&snapshot)
.map_err(|err| format!("invalid runtime snapshot: {err}"))?;
let report = RuntimeStateSummaryReport {
snapshot_id: snapshot.snapshot_id.clone(),
summary: snapshot.summary(),
};
println!("{}", serde_json::to_string_pretty(&report)?);
return Ok(());
}
let input = load_runtime_state_input(snapshot_path)?;
let report = RuntimeStateSummaryReport {
snapshot_id: input.input_id,
summary: RuntimeSummary::from_state(&input.state),
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn diff_state(
left_path: &Path,
right_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let left = load_normalized_runtime_state(left_path)?;
let right = load_normalized_runtime_state(right_path)?;
let differences = diff_json_values(&left, &right);
let report = RuntimeStateDiffReport {
matches: differences.is_empty(),
difference_count: differences.len(),
differences,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn snapshot_state(
input_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let input = load_runtime_state_input(input_path)?;
let snapshot = RuntimeSnapshotDocument {
format_version: SNAPSHOT_FORMAT_VERSION,
snapshot_id: format!("{}-snapshot", input.input_id),
source: RuntimeSnapshotSource {
source_fixture_id: None,
description: Some(match input.description {
Some(description) => format!(
"Runtime snapshot from {} ({description})",
input_path.display()
),
None => format!("Runtime snapshot from {}", input_path.display()),
}),
},
state: input.state,
};
save_runtime_snapshot_document(output_path, &snapshot)?;
let summary = snapshot.summary();
let report = RuntimeStateSummaryReport {
snapshot_id: snapshot.snapshot_id,
summary,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}

View file

@ -0,0 +1,175 @@
use std::fs;
use std::path::Path;
use crate::app::helpers::inspect::build_profile_block_export_document;
use crate::app::reports::inspect::{
RuntimeBuildingTypeInspectionOutput, RuntimeCampaignExeInspectionOutput,
RuntimeCargoEconomyInspectionOutput, RuntimeCargoSelectorInspectionOutput,
RuntimeCargoSkinInspectionOutput, RuntimeCargoTypeInspectionOutput, RuntimePk4ExtractionOutput,
RuntimePk4InspectionOutput, RuntimeProfileBlockExportReport, RuntimeWinInspectionOutput,
};
use rrt_runtime::inspect::{
building::inspect_building_types_dir_with_bindings,
campaign::inspect_campaign_exe_file,
cargo::{
inspect_cargo_economy_sources_with_bindings, inspect_cargo_skin_pk4,
inspect_cargo_types_dir,
},
pk4::{extract_pk4_entry_file, inspect_pk4_file},
smp::bundle::inspect_smp_file,
win::inspect_win_file,
};
pub(crate) fn inspect_pk4(pk4_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimePk4InspectionOutput {
path: pk4_path.display().to_string(),
inspection: inspect_pk4_file(pk4_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_cargo_types(
cargo_types_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeCargoTypeInspectionOutput {
path: cargo_types_dir.display().to_string(),
inspection: inspect_cargo_types_dir(cargo_types_dir)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_building_type_sources(
building_types_dir: &Path,
bindings_path: Option<&Path>,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeBuildingTypeInspectionOutput {
path: building_types_dir.display().to_string(),
inspection: inspect_building_types_dir_with_bindings(building_types_dir, bindings_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_cargo_skins(
cargo_skin_pk4_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeCargoSkinInspectionOutput {
path: cargo_skin_pk4_path.display().to_string(),
inspection: inspect_cargo_skin_pk4(cargo_skin_pk4_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_cargo_economy_sources(
cargo_types_dir: &Path,
cargo_skin_pk4_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let cargo_bindings_path =
Path::new("artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json");
let report = RuntimeCargoEconomyInspectionOutput {
cargo_types_dir: cargo_types_dir.display().to_string(),
cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(),
inspection: inspect_cargo_economy_sources_with_bindings(
cargo_types_dir,
cargo_skin_pk4_path,
Some(cargo_bindings_path),
)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_cargo_production_selector(
cargo_types_dir: &Path,
cargo_skin_pk4_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let cargo_bindings_path =
Path::new("artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json");
let inspection = inspect_cargo_economy_sources_with_bindings(
cargo_types_dir,
cargo_skin_pk4_path,
Some(cargo_bindings_path),
)?;
let selector = inspection
.production_selector
.ok_or("named cargo production selector is not available in the checked-in bindings")?;
let report = RuntimeCargoSelectorInspectionOutput {
cargo_types_dir: cargo_types_dir.display().to_string(),
cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(),
selector,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_cargo_price_selector(
cargo_types_dir: &Path,
cargo_skin_pk4_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let cargo_bindings_path =
Path::new("artifacts/exports/rt3-1.06/event-effects-cargo-bindings.json");
let inspection = inspect_cargo_economy_sources_with_bindings(
cargo_types_dir,
cargo_skin_pk4_path,
Some(cargo_bindings_path),
)?;
let report = RuntimeCargoSelectorInspectionOutput {
cargo_types_dir: cargo_types_dir.display().to_string(),
cargo_skin_pk4_path: cargo_skin_pk4_path.display().to_string(),
selector: inspection.price_selector,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_win(win_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeWinInspectionOutput {
path: win_path.display().to_string(),
inspection: inspect_win_file(win_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn extract_pk4_entry(
pk4_path: &Path,
entry_name: &str,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimePk4ExtractionOutput {
path: pk4_path.display().to_string(),
output_path: output_path.display().to_string(),
extraction: extract_pk4_entry_file(pk4_path, entry_name, output_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_campaign_exe(exe_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeCampaignExeInspectionOutput {
path: exe_path.display().to_string(),
inspection: inspect_campaign_exe_file(exe_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn export_profile_block(
smp_path: &Path,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let inspection = inspect_smp_file(smp_path)?;
let document = build_profile_block_export_document(smp_path, &inspection)?;
let bytes = serde_json::to_vec_pretty(&document)?;
fs::write(output_path, bytes)?;
let report = RuntimeProfileBlockExportReport {
output_path: output_path.display().to_string(),
profile_kind: document.profile_kind,
profile_family: document.profile_family,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}

View file

@ -0,0 +1,128 @@
use std::fs;
use std::path::Path;
use crate::app::helpers::inspect::build_runtime_compact_event_dispatch_cluster_report;
use crate::app::reports::inspect::{
RuntimeCompactEventDispatchClusterCountsOutput, RuntimeCompactEventDispatchClusterCountsReport,
RuntimeCompactEventDispatchClusterOutput, RuntimeMapTitleHintDirectoryOutput,
RuntimeMapTitleHintDirectoryReport, RuntimeMapTitleHintMapEntry,
};
use rrt_runtime::inspect::smp::map_title::inspect_map_title_hint_file;
pub(crate) fn inspect_map_title_hints(root_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut maps = Vec::new();
let mut maps_scanned = 0usize;
let mut maps_with_probe = 0usize;
let mut maps_with_grounded_title_hits = 0usize;
let mut maps_with_adjacent_title_pairs = 0usize;
let mut maps_with_same_stem_adjacent_pairs = 0usize;
let mut paths = fs::read_dir(root_path)?
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| {
path.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case("gmp"))
})
.collect::<Vec<_>>();
paths.sort();
for path in paths {
maps_scanned += 1;
if let Some(probe) = inspect_map_title_hint_file(&path)? {
maps_with_probe += 1;
if !probe.grounded_title_hits.is_empty() {
maps_with_grounded_title_hits += 1;
}
if !probe.adjacent_reference_title_pairs.is_empty() {
maps_with_adjacent_title_pairs += 1;
}
if probe.strongest_same_stem_pair.is_some() {
maps_with_same_stem_adjacent_pairs += 1;
}
maps.push(RuntimeMapTitleHintMapEntry {
path: path.display().to_string(),
probe,
});
}
}
let output = RuntimeMapTitleHintDirectoryOutput {
root_path: root_path.display().to_string(),
report: RuntimeMapTitleHintDirectoryReport {
maps_scanned,
maps_with_probe,
maps_with_grounded_title_hits,
maps_with_adjacent_title_pairs,
maps_with_same_stem_adjacent_pairs,
maps,
},
};
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
pub(crate) fn inspect_compact_event_dispatch_cluster(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = build_runtime_compact_event_dispatch_cluster_report(root_path)?;
let output = RuntimeCompactEventDispatchClusterOutput {
root_path: root_path.display().to_string(),
report,
};
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
pub(crate) fn inspect_compact_event_dispatch_cluster_counts(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = build_runtime_compact_event_dispatch_cluster_report(root_path)?;
let output = RuntimeCompactEventDispatchClusterCountsOutput {
root_path: root_path.display().to_string(),
report: RuntimeCompactEventDispatchClusterCountsReport {
maps_scanned: report.maps_scanned,
maps_with_event_runtime_collection: report.maps_with_event_runtime_collection,
maps_with_dispatch_strip_records: report.maps_with_dispatch_strip_records,
dispatch_strip_record_count: report.dispatch_strip_record_count,
dispatch_strip_records_with_trigger_kind: report
.dispatch_strip_records_with_trigger_kind,
dispatch_strip_records_missing_trigger_kind: report
.dispatch_strip_records_missing_trigger_kind,
dispatch_strip_payload_families: report.dispatch_strip_payload_families,
dispatch_descriptor_occurrence_counts: report.dispatch_descriptor_occurrence_counts,
dispatch_descriptor_map_counts: report.dispatch_descriptor_map_counts,
unknown_descriptor_ids: report.unknown_descriptor_ids,
unknown_descriptor_special_condition_label_matches: report
.unknown_descriptor_special_condition_label_matches,
add_building_dispatch_record_count: report.add_building_dispatch_record_count,
add_building_dispatch_records_with_trigger_kind: report
.add_building_dispatch_records_with_trigger_kind,
add_building_dispatch_records_missing_trigger_kind: report
.add_building_dispatch_records_missing_trigger_kind,
add_building_descriptor_occurrence_counts: report
.add_building_descriptor_occurrence_counts,
add_building_descriptor_map_counts: report.add_building_descriptor_map_counts,
add_building_row_shape_occurrence_counts: report
.add_building_row_shape_occurrence_counts,
add_building_row_shape_map_counts: report.add_building_row_shape_map_counts,
add_building_signature_family_occurrence_counts: report
.add_building_signature_family_occurrence_counts,
add_building_signature_family_map_counts: report
.add_building_signature_family_map_counts,
add_building_condition_tuple_occurrence_counts: report
.add_building_condition_tuple_occurrence_counts,
add_building_condition_tuple_map_counts: report.add_building_condition_tuple_map_counts,
add_building_signature_condition_cluster_occurrence_counts: report
.add_building_signature_condition_cluster_occurrence_counts,
add_building_signature_condition_cluster_map_counts: report
.add_building_signature_condition_cluster_map_counts,
add_building_signature_condition_cluster_descriptor_keys: report
.add_building_signature_condition_cluster_descriptor_keys,
add_building_signature_condition_cluster_non_add_building_descriptor_keys: report
.add_building_signature_condition_cluster_non_add_building_descriptor_keys,
},
};
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}

View file

@ -0,0 +1,19 @@
mod assets;
mod maps;
mod smp;
pub(crate) use assets::{
export_profile_block, extract_pk4_entry, inspect_building_type_sources, inspect_campaign_exe,
inspect_cargo_economy_sources, inspect_cargo_price_selector, inspect_cargo_production_selector,
inspect_cargo_skins, inspect_cargo_types, inspect_pk4, inspect_win,
};
pub(crate) use maps::{
inspect_compact_event_dispatch_cluster, inspect_compact_event_dispatch_cluster_counts,
inspect_map_title_hints,
};
pub(crate) use smp::{
inspect_infrastructure_asset_trace, inspect_periodic_company_service_trace,
inspect_placed_structure_dynamic_side_buffer, inspect_region_service_trace,
inspect_save_company_chairman, inspect_save_placed_structure_triplets,
inspect_save_region_queued_notice_records, inspect_smp, inspect_unclassified_save_collections,
};

View file

@ -0,0 +1,117 @@
use std::path::Path;
use crate::app::reports::inspect::{
RuntimeInfrastructureAssetTraceOutput, RuntimePeriodicCompanyServiceTraceOutput,
RuntimeRegionServiceTraceOutput, RuntimeSaveCompanyChairmanAnalysisOutput,
RuntimeSmpInspectionOutput,
};
use rrt_runtime::inspect::smp::{
bundle::{inspect_smp_file, inspect_unclassified_save_collection_headers_file},
services::{
inspect_save_infrastructure_asset_trace_file,
inspect_save_periodic_company_service_trace_file, inspect_save_region_service_trace_file,
},
structures::{
inspect_save_placed_structure_dynamic_side_buffer_file,
inspect_save_region_queued_notice_records_file,
},
world::inspect_save_company_and_chairman_analysis_file,
};
pub(crate) fn inspect_smp(smp_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeSmpInspectionOutput {
path: smp_path.display().to_string(),
inspection: inspect_smp_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_save_company_chairman(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeSaveCompanyChairmanAnalysisOutput {
path: smp_path.display().to_string(),
analysis: inspect_save_company_and_chairman_analysis_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_save_placed_structure_triplets(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let analysis = inspect_save_company_and_chairman_analysis_file(smp_path)?;
println!(
"{}",
serde_json::to_string_pretty(&analysis.placed_structure_record_triplets)?
);
Ok(())
}
pub(crate) fn inspect_periodic_company_service_trace(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimePeriodicCompanyServiceTraceOutput {
path: smp_path.display().to_string(),
trace: inspect_save_periodic_company_service_trace_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_region_service_trace(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeRegionServiceTraceOutput {
path: smp_path.display().to_string(),
trace: inspect_save_region_service_trace_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_infrastructure_asset_trace(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let report = RuntimeInfrastructureAssetTraceOutput {
path: smp_path.display().to_string(),
trace: inspect_save_infrastructure_asset_trace_file(smp_path)?,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn inspect_save_region_queued_notice_records(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
println!(
"{}",
serde_json::to_string_pretty(&inspect_save_region_queued_notice_records_file(smp_path)?)?
);
Ok(())
}
pub(crate) fn inspect_placed_structure_dynamic_side_buffer(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
println!(
"{}",
serde_json::to_string_pretty(&inspect_save_placed_structure_dynamic_side_buffer_file(
smp_path
)?)?
);
Ok(())
}
pub(crate) fn inspect_unclassified_save_collections(
smp_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
println!(
"{}",
serde_json::to_string_pretty(&inspect_unclassified_save_collection_headers_file(
smp_path
)?)?
);
Ok(())
}

View file

@ -0,0 +1,245 @@
use super::common::{
POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT,
SPECIAL_CONDITION_COUNT, SPECIAL_CONDITIONS_OFFSET, aligned_runtime_rule_known_label,
aligned_runtime_rule_lane_kind, collect_special_conditions_input_paths,
};
use crate::app::runtime_compare::{classify_candidate_table_header_profile, read_u32_le};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
use serde::Serialize;
#[derive(Debug, Clone)]
pub(crate) struct RuntimeAlignedRuntimeRuleBandScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) nonzero_band_indices: Vec<usize>,
pub(crate) values_by_band_index: BTreeMap<usize, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeAlignedRuntimeRuleBandOffsetSummary {
pub(crate) band_index: usize,
pub(crate) relative_offset_hex: String,
pub(crate) lane_kind: String,
pub(crate) known_label: Option<String>,
pub(crate) file_count_present: usize,
pub(crate) distinct_value_count: usize,
pub(crate) sample_value_hexes: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeAlignedRuntimeRuleBandFamilySummary {
pub(crate) profile_family: String,
pub(crate) source_kinds: Vec<String>,
pub(crate) file_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) distinct_nonzero_index_set_count: usize,
pub(crate) stable_nonzero_band_indices: Vec<usize>,
pub(crate) union_nonzero_band_indices: Vec<usize>,
pub(crate) offset_summaries: Vec<RuntimeAlignedRuntimeRuleBandOffsetSummary>,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeAlignedRuntimeRuleBandScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) family_summaries: Vec<RuntimeAlignedRuntimeRuleBandFamilySummary>,
}
pub(crate) fn scan_aligned_runtime_rule_band(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_aligned_runtime_rule_band_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let files_with_any_nonzero_count = samples
.iter()
.filter(|sample| !sample.nonzero_band_indices.is_empty())
.count();
let mut grouped = BTreeMap::<String, Vec<RuntimeAlignedRuntimeRuleBandScanSample>>::new();
for sample in samples {
grouped
.entry(sample.profile_family.clone())
.or_default()
.push(sample);
}
let family_summaries = grouped
.into_iter()
.map(|(profile_family, samples)| {
let file_count = samples.len();
let files_with_any_nonzero_count = samples
.iter()
.filter(|sample| !sample.nonzero_band_indices.is_empty())
.count();
let source_kinds = samples
.iter()
.map(|sample| sample.source_kind.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
let distinct_nonzero_index_set_count = samples
.iter()
.map(|sample| sample.nonzero_band_indices.clone())
.collect::<BTreeSet<_>>()
.len();
let stable_band_indices = if samples.is_empty() {
BTreeSet::new()
} else {
let mut stable = samples[0]
.nonzero_band_indices
.iter()
.copied()
.collect::<BTreeSet<_>>();
for sample in samples.iter().skip(1) {
let current = sample
.nonzero_band_indices
.iter()
.copied()
.collect::<BTreeSet<_>>();
stable = stable.intersection(&current).copied().collect();
}
stable
};
let mut band_values = BTreeMap::<usize, BTreeSet<String>>::new();
let mut band_counts = BTreeMap::<usize, usize>::new();
for sample in &samples {
for band_index in &sample.nonzero_band_indices {
*band_counts.entry(*band_index).or_default() += 1;
}
for (band_index, value_hex) in &sample.values_by_band_index {
band_values
.entry(*band_index)
.or_default()
.insert(value_hex.clone());
}
}
let offset_summaries = band_counts
.into_iter()
.map(
|(band_index, count)| RuntimeAlignedRuntimeRuleBandOffsetSummary {
band_index,
relative_offset_hex: format!("0x{:x}", band_index * 4),
lane_kind: aligned_runtime_rule_lane_kind(band_index).to_string(),
known_label: aligned_runtime_rule_known_label(band_index)
.map(str::to_string),
file_count_present: count,
distinct_value_count: band_values
.get(&band_index)
.map(BTreeSet::len)
.unwrap_or(0),
sample_value_hexes: band_values
.get(&band_index)
.map(|values| values.iter().take(8).cloned().collect())
.unwrap_or_default(),
},
)
.collect::<Vec<_>>();
RuntimeAlignedRuntimeRuleBandFamilySummary {
profile_family,
source_kinds,
file_count,
files_with_any_nonzero_count,
distinct_nonzero_index_set_count,
stable_nonzero_band_indices: stable_band_indices.into_iter().collect(),
union_nonzero_band_indices: band_values.keys().copied().collect(),
offset_summaries,
sample_paths: samples
.iter()
.take(12)
.map(|sample| sample.path.clone())
.collect(),
}
})
.collect::<Vec<_>>();
let report = RuntimeAlignedRuntimeRuleBandScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_nonzero_count,
skipped_file_count,
family_summaries,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_aligned_runtime_rule_band_scan_sample(
smp_path: &Path,
) -> Result<RuntimeAlignedRuntimeRuleBandScanSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let table_len = SPECIAL_CONDITION_COUNT * 4;
let table_end = SPECIAL_CONDITIONS_OFFSET
.checked_add(table_len)
.ok_or("special-conditions table overflow")?;
if bytes.len() < POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET || bytes.len() < table_end {
return Err(format!(
"{} is too small for the fixed aligned-runtime-rule band",
smp_path.display()
)
.into());
}
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let profile_family = classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let source_kind = match extension.as_str() {
"gmp" => "map-aligned-runtime-rule-band",
"gms" => "save-aligned-runtime-rule-band",
"gmx" => "sandbox-aligned-runtime-rule-band",
_ => "aligned-runtime-rule-band",
}
.to_string();
let mut nonzero_band_indices = Vec::new();
let mut values_by_band_index = BTreeMap::new();
for band_index in 0..SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT {
let offset = SPECIAL_CONDITIONS_OFFSET + band_index * 4;
let value = read_u32_le(&bytes, offset).ok_or_else(|| {
format!(
"{} is truncated inside the aligned-runtime-rule band",
smp_path.display()
)
})?;
if value == 0 {
continue;
}
nonzero_band_indices.push(band_index);
values_by_band_index.insert(band_index, format!("0x{value:08x}"));
}
Ok(RuntimeAlignedRuntimeRuleBandScanSample {
path: smp_path.display().to_string(),
profile_family,
source_kind,
nonzero_band_indices,
values_by_band_index,
})
}

View file

@ -0,0 +1,388 @@
use crate::app::runtime_compare::{
RuntimeCandidateTableNamedRun, classify_candidate_table_header_profile,
collect_numbered_candidate_name_runs, load_candidate_table_inspection_report,
matches_candidate_table_header_bytes, read_u32_le,
};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableHeaderCluster {
pub(crate) header_word_0_hex: String,
pub(crate) header_word_1_hex: String,
pub(crate) file_count: usize,
pub(crate) profile_families: Vec<String>,
pub(crate) source_kinds: Vec<String>,
pub(crate) zero_trailer_count_min: usize,
pub(crate) zero_trailer_count_max: usize,
pub(crate) zero_trailer_count_values: Vec<usize>,
pub(crate) distinct_zero_name_set_count: usize,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableHeaderScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) cluster_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) clusters: Vec<RuntimeCandidateTableHeaderCluster>,
}
#[derive(Debug, Clone)]
pub(crate) struct RuntimeCandidateTableHeaderScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) header_word_0_hex: String,
pub(crate) header_word_1_hex: String,
pub(crate) zero_trailer_entry_count: usize,
pub(crate) zero_trailer_entry_names: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeCandidateTableNamedRunScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) observed_entry_count: usize,
pub(crate) port_runs: Vec<RuntimeCandidateTableNamedRun>,
pub(crate) warehouse_runs: Vec<RuntimeCandidateTableNamedRun>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeCandidateTableNamedRunScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_numbered_port_runs_count: usize,
pub(crate) files_with_any_numbered_warehouse_runs_count: usize,
pub(crate) files_with_both_numbered_run_families_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) samples: Vec<RuntimeCandidateTableNamedRunScanSample>,
}
pub(crate) fn scan_candidate_table_headers(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_candidate_table_input_paths(root_path, &mut candidate_paths)?;
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_candidate_table_header_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let mut grouped =
BTreeMap::<(String, String), Vec<RuntimeCandidateTableHeaderScanSample>>::new();
for sample in samples {
grouped
.entry((
sample.header_word_0_hex.clone(),
sample.header_word_1_hex.clone(),
))
.or_default()
.push(sample);
}
let file_count = grouped.values().map(Vec::len).sum();
let clusters = grouped
.into_iter()
.map(|((header_word_0_hex, header_word_1_hex), samples)| {
let mut profile_families = samples
.iter()
.map(|sample| sample.profile_family.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
let mut source_kinds = samples
.iter()
.map(|sample| sample.source_kind.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
let mut zero_trailer_count_values = samples
.iter()
.map(|sample| sample.zero_trailer_entry_count)
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
let distinct_zero_name_set_count = samples
.iter()
.map(|sample| sample.zero_trailer_entry_names.clone())
.collect::<BTreeSet<_>>()
.len();
let zero_trailer_count_min = samples
.iter()
.map(|sample| sample.zero_trailer_entry_count)
.min()
.unwrap_or(0);
let zero_trailer_count_max = samples
.iter()
.map(|sample| sample.zero_trailer_entry_count)
.max()
.unwrap_or(0);
let sample_paths = samples
.iter()
.take(12)
.map(|sample| sample.path.clone())
.collect::<Vec<_>>();
profile_families.sort();
source_kinds.sort();
zero_trailer_count_values.sort();
RuntimeCandidateTableHeaderCluster {
header_word_0_hex,
header_word_1_hex,
file_count: samples.len(),
profile_families,
source_kinds,
zero_trailer_count_min,
zero_trailer_count_max,
zero_trailer_count_values,
distinct_zero_name_set_count,
sample_paths,
}
})
.collect::<Vec<_>>();
let report = RuntimeCandidateTableHeaderScanReport {
root_path: root_path.display().to_string(),
file_count,
cluster_count: clusters.len(),
skipped_file_count,
clusters,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn scan_candidate_table_named_runs(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_candidate_table_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_candidate_table_named_run_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let files_with_any_numbered_port_runs_count = samples
.iter()
.filter(|sample| !sample.port_runs.is_empty())
.count();
let files_with_any_numbered_warehouse_runs_count = samples
.iter()
.filter(|sample| !sample.warehouse_runs.is_empty())
.count();
let files_with_both_numbered_run_families_count = samples
.iter()
.filter(|sample| !sample.port_runs.is_empty() && !sample.warehouse_runs.is_empty())
.count();
let report = RuntimeCandidateTableNamedRunScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_numbered_port_runs_count,
files_with_any_numbered_warehouse_runs_count,
files_with_both_numbered_run_families_count,
skipped_file_count,
samples,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn collect_candidate_table_input_paths(
root_path: &Path,
out: &mut Vec<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
let metadata = match fs::symlink_metadata(root_path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
if metadata.file_type().is_symlink() {
return Ok(());
}
if root_path.is_file() {
if root_path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms"))
{
out.push(root_path.to_path_buf());
}
return Ok(());
}
let entries = match fs::read_dir(root_path) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_candidate_table_input_paths(&path, out)?;
continue;
}
if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms"))
{
out.push(path);
}
}
Ok(())
}
pub(crate) fn load_candidate_table_header_scan_sample(
smp_path: &Path,
) -> Result<RuntimeCandidateTableHeaderScanSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let header_offset = 0x6a70usize;
let entries_offset = 0x6ad1usize;
let block_end_offset = 0x73c0usize;
let entry_stride = 0x22usize;
if bytes.len() < block_end_offset {
return Err(format!(
"{} is too small for the fixed candidate table range",
smp_path.display()
)
.into());
}
if !matches_candidate_table_header_bytes(&bytes, header_offset) {
return Err(format!(
"{} does not contain the fixed candidate table header",
smp_path.display()
)
.into());
}
let observed_entry_capacity = read_u32_le(&bytes, header_offset + 0x1c)
.ok_or_else(|| format!("{} is missing candidate table capacity", smp_path.display()))?
as usize;
let observed_entry_count = read_u32_le(&bytes, header_offset + 0x20)
.ok_or_else(|| format!("{} is missing candidate table count", smp_path.display()))?
as usize;
if observed_entry_capacity < observed_entry_count {
return Err(format!(
"{} has invalid candidate table capacity/count {observed_entry_capacity}/{observed_entry_count}",
smp_path.display()
)
.into());
}
let entries_end_offset = entries_offset
.checked_add(
observed_entry_count
.checked_mul(entry_stride)
.ok_or("candidate table length overflow")?,
)
.ok_or("candidate table end overflow")?;
if entries_end_offset > block_end_offset {
return Err(format!(
"{} candidate table overruns fixed block end",
smp_path.display()
)
.into());
}
let mut zero_trailer_entry_names = Vec::new();
for index in 0..observed_entry_count {
let offset = entries_offset + index * entry_stride;
let chunk = &bytes[offset..offset + entry_stride];
let nul_index = chunk
.iter()
.position(|byte| *byte == 0)
.unwrap_or(entry_stride - 4);
let text = std::str::from_utf8(&chunk[..nul_index]).map_err(|_| {
format!(
"{} contains invalid UTF-8 in candidate table",
smp_path.display()
)
})?;
let availability = read_u32_le(&bytes, offset + entry_stride - 4).ok_or_else(|| {
format!(
"{} is missing candidate availability dword",
smp_path.display()
)
})?;
if availability == 0 {
zero_trailer_entry_names.push(text.to_string());
}
}
let profile_family = classify_candidate_table_header_profile(
smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase()),
&bytes,
);
let source_kind = match smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.as_deref()
{
Some("gmp") => "map-fixed-catalog-range",
Some("gms") => "save-fixed-catalog-range",
_ => "fixed-catalog-range",
}
.to_string();
Ok(RuntimeCandidateTableHeaderScanSample {
path: smp_path.display().to_string(),
profile_family,
source_kind,
header_word_0_hex: format!(
"0x{:08x}",
read_u32_le(&bytes, header_offset).ok_or("missing candidate header word 0")?
),
header_word_1_hex: format!(
"0x{:08x}",
read_u32_le(&bytes, header_offset + 4).ok_or("missing candidate header word 1")?
),
zero_trailer_entry_count: zero_trailer_entry_names.len(),
zero_trailer_entry_names,
})
}
pub(crate) fn load_candidate_table_named_run_scan_sample(
smp_path: &Path,
) -> Result<RuntimeCandidateTableNamedRunScanSample, Box<dyn std::error::Error>> {
let report = load_candidate_table_inspection_report(smp_path)?;
let port_runs = collect_numbered_candidate_name_runs(&report.entries, "Port");
let warehouse_runs = collect_numbered_candidate_name_runs(&report.entries, "Warehouse");
Ok(RuntimeCandidateTableNamedRunScanSample {
path: report.path,
profile_family: report.profile_family,
source_kind: report.source_kind,
observed_entry_count: report.observed_entry_count,
port_runs,
warehouse_runs,
})
}

View file

@ -0,0 +1,130 @@
use std::fs;
use std::path::{Path, PathBuf};
pub(crate) const SPECIAL_CONDITIONS_OFFSET: usize = 0x0d64;
pub(crate) const SPECIAL_CONDITION_COUNT: usize = 36;
pub(crate) const SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT: usize = 35;
pub(crate) const SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT: usize = 50;
pub(crate) const SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT: usize = 49;
pub(crate) const SMP_ALIGNED_RUNTIME_RULE_END_OFFSET: usize =
SPECIAL_CONDITIONS_OFFSET + SMP_ALIGNED_RUNTIME_RULE_DWORD_COUNT * 4;
pub(crate) const POST_SPECIAL_CONDITIONS_SCALAR_OFFSET: usize = 0x0df4;
pub(crate) const POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET: usize = 0x0f30;
pub(crate) const POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET: usize =
SMP_ALIGNED_RUNTIME_RULE_END_OFFSET;
pub(crate) const SPECIAL_CONDITION_LABELS: [&str; SPECIAL_CONDITION_COUNT] = [
"Disable Stock Buying and Selling",
"Disable Margin Buying/Short Selling Stock",
"Disable Company Issue/Buy Back Stock",
"Disable Issuing/Repaying Bonds",
"Disable Declaring Bankruptcy",
"Disable Changing the Dividend Rate",
"Disable Replacing a Locomotive",
"Disable Retiring a Train",
"Disable Changing Cargo Consist On Train",
"Disable Buying a Train",
"Disable All Track Building",
"Disable Unconnected Track Building",
"Limited Track Building Amount",
"Disable Building Stations",
"Disable Building Hotel/Restaurant/Tavern/Post Office",
"Disable Building Customs House",
"Disable Building Industry Buildings",
"Disable Buying Existing Industry Buildings",
"Disable Being Fired As Chairman",
"Disable Resigning as Chairman",
"Disable Chairmanship Takeover",
"Disable Starting Any Companies",
"Disable Starting Multiple Companies",
"Disable Merging Companies",
"Disable Bulldozing",
"Show Visited Track",
"Show Visited Stations",
"Use Slow Date",
"Completely Disable Money-Related Things",
"Use Bio-Accelerator Cars",
"Disable Cargo Economy",
"Use Wartime Cargos",
"Disable Train Crashes",
"Disable Train Crashes AND Breakdowns",
"AI Ignore Territories At Startup",
"Hidden sentinel",
];
pub(crate) fn collect_special_conditions_input_paths(
root_path: &Path,
out: &mut Vec<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
let metadata = match fs::symlink_metadata(root_path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
if metadata.file_type().is_symlink() {
return Ok(());
}
if root_path.is_file() {
if root_path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms" | "gmx"))
{
out.push(root_path.to_path_buf());
}
return Ok(());
}
let entries = match fs::read_dir(root_path) {
Ok(entries) => entries,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return Ok(()),
Err(err) => return Err(err.into()),
};
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_special_conditions_input_paths(&path, out)?;
continue;
}
if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| matches!(ext.to_ascii_lowercase().as_str(), "gmp" | "gms" | "gmx"))
{
out.push(path);
}
}
Ok(())
}
pub(crate) fn parse_special_condition_slot_index(label: &str) -> Option<u8> {
let suffix = label.strip_prefix("slot ")?;
let (slot_index, _) = suffix.split_once(':')?;
slot_index.parse().ok()
}
pub(crate) fn parse_hex_offset(text: &str) -> Option<usize> {
text.strip_prefix("0x")
.and_then(|digits| usize::from_str_radix(digits, 16).ok())
}
pub(crate) fn aligned_runtime_rule_lane_kind(band_index: usize) -> &'static str {
if band_index < SPECIAL_CONDITION_COUNT {
"known-special-condition-dword"
} else if band_index < SMP_ALIGNED_RUNTIME_RULE_KNOWN_EDITOR_RULE_COUNT {
"unlabeled-editor-rule-dword"
} else {
"trailing-runtime-scalar"
}
}
pub(crate) fn aligned_runtime_rule_known_label(band_index: usize) -> Option<&'static str> {
if band_index < SPECIAL_CONDITION_LABELS.len() {
Some(SPECIAL_CONDITION_LABELS[band_index])
} else {
None
}
}

View file

@ -0,0 +1,15 @@
pub(crate) mod common;
pub(crate) mod post_special;
mod aligned_band;
mod candidate_table;
mod recipe_book;
mod special_conditions;
pub(super) use aligned_band::scan_aligned_runtime_rule_band;
pub(super) use candidate_table::{scan_candidate_table_headers, scan_candidate_table_named_runs};
pub(super) use post_special::{
scan_post_special_conditions_scalars, scan_post_special_conditions_tail,
};
pub(super) use recipe_book::scan_recipe_book_lines;
pub(super) use special_conditions::scan_special_conditions;

View file

@ -0,0 +1,489 @@
use super::common::{
POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET, POST_SPECIAL_CONDITIONS_SCALAR_OFFSET,
POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET, SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT,
SPECIAL_CONDITIONS_OFFSET, collect_special_conditions_input_paths, parse_hex_offset,
};
use crate::app::runtime_compare::{classify_candidate_table_header_profile, read_u32_le};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
use serde::Serialize;
#[derive(Debug, Clone)]
pub(crate) struct RuntimePostSpecialConditionsScalarScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) nonzero_relative_offsets: Vec<usize>,
pub(crate) values_by_relative_offset_hex: BTreeMap<String, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarOffsetSummary {
pub(crate) relative_offset_hex: String,
pub(crate) file_count_present: usize,
pub(crate) distinct_value_count: usize,
pub(crate) sample_value_hexes: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarFamilySummary {
pub(crate) profile_family: String,
pub(crate) source_kinds: Vec<String>,
pub(crate) file_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) distinct_nonzero_offset_set_count: usize,
pub(crate) stable_nonzero_relative_offset_hexes: Vec<String>,
pub(crate) union_nonzero_relative_offset_hexes: Vec<String>,
pub(crate) offset_summaries: Vec<RuntimePostSpecialConditionsScalarOffsetSummary>,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsScalarScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) family_summaries: Vec<RuntimePostSpecialConditionsScalarFamilySummary>,
}
#[derive(Debug, Clone)]
pub(crate) struct RuntimePostSpecialConditionsTailScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) nonzero_relative_offsets: Vec<usize>,
pub(crate) values_by_relative_offset_hex: BTreeMap<String, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsTailOffsetSummary {
pub(crate) relative_offset_hex: String,
pub(crate) file_count_present: usize,
pub(crate) distinct_value_count: usize,
pub(crate) sample_value_hexes: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsTailFamilySummary {
pub(crate) profile_family: String,
pub(crate) source_kinds: Vec<String>,
pub(crate) file_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) distinct_nonzero_offset_set_count: usize,
pub(crate) stable_nonzero_relative_offset_hexes: Vec<String>,
pub(crate) union_nonzero_relative_offset_hexes: Vec<String>,
pub(crate) offset_summaries: Vec<RuntimePostSpecialConditionsTailOffsetSummary>,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimePostSpecialConditionsTailScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_nonzero_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) family_summaries: Vec<RuntimePostSpecialConditionsTailFamilySummary>,
}
pub(crate) fn scan_post_special_conditions_scalars(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_post_special_conditions_scalar_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let files_with_any_nonzero_count = samples
.iter()
.filter(|sample| !sample.nonzero_relative_offsets.is_empty())
.count();
let family_summaries = build_scalar_family_summaries(samples);
let report = RuntimePostSpecialConditionsScalarScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_nonzero_count,
skipped_file_count,
family_summaries,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn scan_post_special_conditions_tail(
root_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_post_special_conditions_tail_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let files_with_any_nonzero_count = samples
.iter()
.filter(|sample| !sample.nonzero_relative_offsets.is_empty())
.count();
let family_summaries = build_tail_family_summaries(samples);
let report = RuntimePostSpecialConditionsTailScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_nonzero_count,
skipped_file_count,
family_summaries,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_post_special_conditions_scalar_scan_sample(
smp_path: &Path,
) -> Result<RuntimePostSpecialConditionsScalarScanSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let table_end = SPECIAL_CONDITIONS_OFFSET + 36 * 4;
if bytes.len() < POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET || bytes.len() < table_end {
return Err(format!(
"{} is too small for the fixed post-special-conditions scalar window",
smp_path.display()
)
.into());
}
let hidden_sentinel = read_u32_le(
&bytes,
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4,
)
.ok_or_else(|| {
format!(
"{} is missing the hidden special-condition sentinel",
smp_path.display()
)
})?;
if hidden_sentinel != 1 {
return Err(format!(
"{} does not match the fixed special-conditions table sentinel",
smp_path.display()
)
.into());
}
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let profile_family = classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let source_kind = match extension.as_str() {
"gmp" => "map-post-special-conditions-window",
"gms" => "save-post-special-conditions-window",
"gmx" => "sandbox-post-special-conditions-window",
_ => "post-special-conditions-window",
}
.to_string();
let mut nonzero_relative_offsets = Vec::new();
let mut values_by_relative_offset_hex = BTreeMap::new();
for offset in (POST_SPECIAL_CONDITIONS_SCALAR_OFFSET..POST_SPECIAL_CONDITIONS_SCALAR_END_OFFSET)
.step_by(4)
{
let value = read_u32_le(&bytes, offset).ok_or_else(|| {
format!(
"{} is truncated inside the fixed post-special-conditions scalar window",
smp_path.display()
)
})?;
if value == 0 {
continue;
}
let relative_offset = offset - POST_SPECIAL_CONDITIONS_SCALAR_OFFSET;
nonzero_relative_offsets.push(relative_offset);
values_by_relative_offset_hex
.insert(format!("0x{relative_offset:x}"), format!("0x{value:08x}"));
}
Ok(RuntimePostSpecialConditionsScalarScanSample {
path: smp_path.display().to_string(),
profile_family,
source_kind,
nonzero_relative_offsets,
values_by_relative_offset_hex,
})
}
pub(crate) fn load_post_special_conditions_tail_scan_sample(
smp_path: &Path,
) -> Result<RuntimePostSpecialConditionsTailScanSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
if bytes.len() < POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET {
return Err(format!(
"{} is too small for the fixed post-special-conditions tail window",
smp_path.display()
)
.into());
}
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let profile_family = classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let source_kind = match extension.as_str() {
"gmp" => "map-post-special-conditions-tail",
"gms" => "save-post-special-conditions-tail",
"gmx" => "sandbox-post-special-conditions-tail",
_ => "post-special-conditions-tail",
}
.to_string();
let mut nonzero_relative_offsets = Vec::new();
let mut values_by_relative_offset_hex = BTreeMap::new();
for offset in (POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET..bytes.len()).step_by(4) {
let Some(value) = read_u32_le(&bytes, offset) else {
break;
};
if value == 0 {
continue;
}
let relative_offset = offset - POST_SPECIAL_CONDITIONS_SCALAR_TAIL_OFFSET;
nonzero_relative_offsets.push(relative_offset);
values_by_relative_offset_hex
.insert(format!("0x{relative_offset:x}"), format!("0x{value:08x}"));
}
Ok(RuntimePostSpecialConditionsTailScanSample {
path: smp_path.display().to_string(),
profile_family,
source_kind,
nonzero_relative_offsets,
values_by_relative_offset_hex,
})
}
fn build_scalar_family_summaries(
samples: Vec<RuntimePostSpecialConditionsScalarScanSample>,
) -> Vec<RuntimePostSpecialConditionsScalarFamilySummary> {
let mut grouped = BTreeMap::<String, Vec<RuntimePostSpecialConditionsScalarScanSample>>::new();
for sample in samples {
grouped
.entry(sample.profile_family.clone())
.or_default()
.push(sample);
}
grouped
.into_iter()
.map(
|(profile_family, samples)| RuntimePostSpecialConditionsScalarFamilySummary {
profile_family,
source_kinds: samples
.iter()
.map(|sample| sample.source_kind.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
file_count: samples.len(),
files_with_any_nonzero_count: samples
.iter()
.filter(|sample| !sample.nonzero_relative_offsets.is_empty())
.count(),
distinct_nonzero_offset_set_count: samples
.iter()
.map(|sample| sample.nonzero_relative_offsets.clone())
.collect::<BTreeSet<_>>()
.len(),
stable_nonzero_relative_offset_hexes: stable_offsets(
samples
.iter()
.map(|sample| sample.nonzero_relative_offsets.as_slice()),
)
.into_iter()
.map(|offset| format!("0x{offset:x}"))
.collect(),
union_nonzero_relative_offset_hexes: collect_offset_values(
samples
.iter()
.map(|sample| &sample.values_by_relative_offset_hex),
)
.0
.keys()
.copied()
.map(|offset| format!("0x{offset:x}"))
.collect(),
offset_summaries: build_offset_summaries(collect_offset_values(
samples
.iter()
.map(|sample| &sample.values_by_relative_offset_hex),
)),
sample_paths: samples
.iter()
.take(12)
.map(|sample| sample.path.clone())
.collect(),
},
)
.collect()
}
fn build_tail_family_summaries(
samples: Vec<RuntimePostSpecialConditionsTailScanSample>,
) -> Vec<RuntimePostSpecialConditionsTailFamilySummary> {
let mut grouped = BTreeMap::<String, Vec<RuntimePostSpecialConditionsTailScanSample>>::new();
for sample in samples {
grouped
.entry(sample.profile_family.clone())
.or_default()
.push(sample);
}
grouped
.into_iter()
.map(
|(profile_family, samples)| RuntimePostSpecialConditionsTailFamilySummary {
profile_family,
source_kinds: samples
.iter()
.map(|sample| sample.source_kind.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
file_count: samples.len(),
files_with_any_nonzero_count: samples
.iter()
.filter(|sample| !sample.nonzero_relative_offsets.is_empty())
.count(),
distinct_nonzero_offset_set_count: samples
.iter()
.map(|sample| sample.nonzero_relative_offsets.clone())
.collect::<BTreeSet<_>>()
.len(),
stable_nonzero_relative_offset_hexes: stable_offsets(
samples
.iter()
.map(|sample| sample.nonzero_relative_offsets.as_slice()),
)
.into_iter()
.map(|offset| format!("0x{offset:x}"))
.collect(),
union_nonzero_relative_offset_hexes: collect_offset_values(
samples
.iter()
.map(|sample| &sample.values_by_relative_offset_hex),
)
.0
.keys()
.copied()
.map(|offset| format!("0x{offset:x}"))
.collect(),
offset_summaries: build_tail_offset_summaries(collect_offset_values(
samples
.iter()
.map(|sample| &sample.values_by_relative_offset_hex),
)),
sample_paths: samples
.iter()
.take(12)
.map(|sample| sample.path.clone())
.collect(),
},
)
.collect()
}
fn stable_offsets<'a>(offset_sets: impl Iterator<Item = &'a [usize]>) -> BTreeSet<usize> {
let mut offset_sets = offset_sets.peekable();
if offset_sets.peek().is_none() {
return BTreeSet::new();
}
let mut stable = offset_sets
.next()
.map(|set| set.iter().copied().collect::<BTreeSet<_>>())
.unwrap_or_default();
for offsets in offset_sets {
let current = offsets.iter().copied().collect::<BTreeSet<_>>();
stable = stable.intersection(&current).copied().collect();
}
stable
}
fn collect_offset_values<'a>(
maps: impl Iterator<Item = &'a BTreeMap<String, String>>,
) -> (BTreeMap<usize, BTreeSet<String>>, BTreeMap<usize, usize>) {
let mut offset_values = BTreeMap::<usize, BTreeSet<String>>::new();
let mut offset_counts = BTreeMap::<usize, usize>::new();
for map in maps {
for (offset_hex, value_hex) in map {
if let Some(offset) = parse_hex_offset(offset_hex) {
*offset_counts.entry(offset).or_default() += 1;
offset_values
.entry(offset)
.or_default()
.insert(value_hex.clone());
}
}
}
(offset_values, offset_counts)
}
fn build_offset_summaries(
(offset_values, offset_counts): (BTreeMap<usize, BTreeSet<String>>, BTreeMap<usize, usize>),
) -> Vec<RuntimePostSpecialConditionsScalarOffsetSummary> {
offset_counts
.into_iter()
.map(
|(offset, count)| RuntimePostSpecialConditionsScalarOffsetSummary {
relative_offset_hex: format!("0x{offset:x}"),
file_count_present: count,
distinct_value_count: offset_values.get(&offset).map(BTreeSet::len).unwrap_or(0),
sample_value_hexes: offset_values
.get(&offset)
.map(|values| values.iter().take(8).cloned().collect())
.unwrap_or_default(),
},
)
.collect()
}
fn build_tail_offset_summaries(
(offset_values, offset_counts): (BTreeMap<usize, BTreeSet<String>>, BTreeMap<usize, usize>),
) -> Vec<RuntimePostSpecialConditionsTailOffsetSummary> {
offset_counts
.into_iter()
.map(
|(offset, count)| RuntimePostSpecialConditionsTailOffsetSummary {
relative_offset_hex: format!("0x{offset:x}"),
file_count_present: count,
distinct_value_count: offset_values.get(&offset).map(BTreeSet::len).unwrap_or(0),
sample_value_hexes: offset_values
.get(&offset)
.map(|values| values.iter().take(8).cloned().collect())
.unwrap_or_default(),
},
)
.collect()
}

View file

@ -0,0 +1,183 @@
use super::common::collect_special_conditions_input_paths;
use crate::app::runtime_compare::{
RuntimeRecipeBookLineFieldSummary, build_recipe_line_field_summaries,
intersect_nonzero_recipe_line_paths, load_recipe_book_line_sample,
};
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use serde::Serialize;
#[derive(Debug, Clone)]
pub(crate) struct RuntimeRecipeBookLineScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) nonzero_mode_paths: BTreeMap<String, String>,
pub(crate) nonzero_supplied_token_paths: BTreeMap<String, String>,
pub(crate) nonzero_demanded_token_paths: BTreeMap<String, String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRecipeBookLineFamilySummary {
pub(crate) profile_family: String,
pub(crate) source_kinds: Vec<String>,
pub(crate) file_count: usize,
pub(crate) files_with_any_nonzero_modes_count: usize,
pub(crate) files_with_any_nonzero_supplied_tokens_count: usize,
pub(crate) files_with_any_nonzero_demanded_tokens_count: usize,
pub(crate) stable_nonzero_mode_paths: Vec<String>,
pub(crate) stable_nonzero_supplied_token_paths: Vec<String>,
pub(crate) stable_nonzero_demanded_token_paths: Vec<String>,
pub(crate) mode_summaries: Vec<RuntimeRecipeBookLineFieldSummary>,
pub(crate) supplied_token_summaries: Vec<RuntimeRecipeBookLineFieldSummary>,
pub(crate) demanded_token_summaries: Vec<RuntimeRecipeBookLineFieldSummary>,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeRecipeBookLineScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_nonzero_modes_count: usize,
pub(crate) files_with_any_nonzero_supplied_tokens_count: usize,
pub(crate) files_with_any_nonzero_demanded_tokens_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) family_summaries: Vec<RuntimeRecipeBookLineFamilySummary>,
}
pub(crate) fn scan_recipe_book_lines(root_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_recipe_book_line_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let files_with_any_nonzero_modes_count = samples
.iter()
.filter(|sample| !sample.nonzero_mode_paths.is_empty())
.count();
let files_with_any_nonzero_supplied_tokens_count = samples
.iter()
.filter(|sample| !sample.nonzero_supplied_token_paths.is_empty())
.count();
let files_with_any_nonzero_demanded_tokens_count = samples
.iter()
.filter(|sample| !sample.nonzero_demanded_token_paths.is_empty())
.count();
let mut grouped = BTreeMap::<String, Vec<RuntimeRecipeBookLineScanSample>>::new();
for sample in samples {
grouped
.entry(sample.profile_family.clone())
.or_default()
.push(sample);
}
let family_summaries = grouped
.into_iter()
.map(
|(profile_family, samples)| RuntimeRecipeBookLineFamilySummary {
profile_family,
source_kinds: samples
.iter()
.map(|sample| sample.source_kind.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
file_count: samples.len(),
files_with_any_nonzero_modes_count: samples
.iter()
.filter(|sample| !sample.nonzero_mode_paths.is_empty())
.count(),
files_with_any_nonzero_supplied_tokens_count: samples
.iter()
.filter(|sample| !sample.nonzero_supplied_token_paths.is_empty())
.count(),
files_with_any_nonzero_demanded_tokens_count: samples
.iter()
.filter(|sample| !sample.nonzero_demanded_token_paths.is_empty())
.count(),
stable_nonzero_mode_paths: intersect_nonzero_recipe_line_paths(
samples.iter().map(|sample| &sample.nonzero_mode_paths),
),
stable_nonzero_supplied_token_paths: intersect_nonzero_recipe_line_paths(
samples
.iter()
.map(|sample| &sample.nonzero_supplied_token_paths),
),
stable_nonzero_demanded_token_paths: intersect_nonzero_recipe_line_paths(
samples
.iter()
.map(|sample| &sample.nonzero_demanded_token_paths),
),
mode_summaries: build_recipe_line_field_summaries(
samples.iter().map(|sample| &sample.nonzero_mode_paths),
),
supplied_token_summaries: build_recipe_line_field_summaries(
samples
.iter()
.map(|sample| &sample.nonzero_supplied_token_paths),
),
demanded_token_summaries: build_recipe_line_field_summaries(
samples
.iter()
.map(|sample| &sample.nonzero_demanded_token_paths),
),
sample_paths: samples
.iter()
.take(12)
.map(|sample| sample.path.clone())
.collect(),
},
)
.collect::<Vec<_>>();
let report = RuntimeRecipeBookLineScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_nonzero_modes_count,
files_with_any_nonzero_supplied_tokens_count,
files_with_any_nonzero_demanded_tokens_count,
skipped_file_count,
family_summaries,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_recipe_book_line_scan_sample(
smp_path: &Path,
) -> Result<RuntimeRecipeBookLineScanSample, Box<dyn std::error::Error>> {
let sample = load_recipe_book_line_sample(smp_path)?;
Ok(RuntimeRecipeBookLineScanSample {
path: sample.path,
profile_family: sample.profile_family,
source_kind: sample.source_kind,
nonzero_mode_paths: sample
.mode_word_hex_by_path
.into_iter()
.filter(|(_, value)| value != "0x00000000")
.collect(),
nonzero_supplied_token_paths: sample
.supplied_cargo_token_word_hex_by_path
.into_iter()
.filter(|(_, value)| value != "0x00000000")
.collect(),
nonzero_demanded_token_paths: sample
.demanded_cargo_token_word_hex_by_path
.into_iter()
.filter(|(_, value)| value != "0x00000000")
.collect(),
})
}

View file

@ -0,0 +1,167 @@
use super::common::{
SPECIAL_CONDITION_COUNT, SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT, SPECIAL_CONDITION_LABELS,
SPECIAL_CONDITIONS_OFFSET, collect_special_conditions_input_paths,
parse_special_condition_slot_index,
};
use crate::app::runtime_compare::{classify_candidate_table_header_profile, read_u32_le};
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub(crate) struct RuntimeSpecialConditionsScanSample {
pub(crate) path: String,
pub(crate) profile_family: String,
pub(crate) source_kind: String,
pub(crate) enabled_visible_count: usize,
pub(crate) enabled_visible_labels: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSpecialConditionsSlotSummary {
pub(crate) slot_index: u8,
pub(crate) label: String,
pub(crate) file_count_enabled: usize,
pub(crate) sample_paths: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(crate) struct RuntimeSpecialConditionsScanReport {
pub(crate) root_path: String,
pub(crate) file_count: usize,
pub(crate) files_with_probe_count: usize,
pub(crate) files_with_any_enabled_count: usize,
pub(crate) skipped_file_count: usize,
pub(crate) enabled_slot_summaries: Vec<RuntimeSpecialConditionsSlotSummary>,
pub(crate) sample_files_with_any_enabled: Vec<RuntimeSpecialConditionsScanSample>,
}
pub(crate) fn scan_special_conditions(root_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut candidate_paths = Vec::new();
collect_special_conditions_input_paths(root_path, &mut candidate_paths)?;
let file_count = candidate_paths.len();
let mut samples = Vec::new();
let mut skipped_file_count = 0usize;
for path in candidate_paths {
match load_special_conditions_scan_sample(&path) {
Ok(sample) => samples.push(sample),
Err(_) => skipped_file_count += 1,
}
}
let files_with_probe_count = samples.len();
let sample_files_with_any_enabled = samples
.iter()
.filter(|sample| sample.enabled_visible_count != 0)
.cloned()
.collect::<Vec<_>>();
let files_with_any_enabled_count = sample_files_with_any_enabled.len();
let mut grouped = BTreeMap::<(u8, String), Vec<String>>::new();
for sample in &samples {
for label in &sample.enabled_visible_labels {
if let Some(slot_index) = parse_special_condition_slot_index(label) {
grouped
.entry((slot_index, label.clone()))
.or_default()
.push(sample.path.clone());
}
}
}
let enabled_slot_summaries = grouped
.into_iter()
.map(
|((slot_index, label), paths)| RuntimeSpecialConditionsSlotSummary {
slot_index,
label,
file_count_enabled: paths.len(),
sample_paths: paths.into_iter().take(12).collect(),
},
)
.collect::<Vec<_>>();
let report = RuntimeSpecialConditionsScanReport {
root_path: root_path.display().to_string(),
file_count,
files_with_probe_count,
files_with_any_enabled_count,
skipped_file_count,
enabled_slot_summaries,
sample_files_with_any_enabled,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
pub(crate) fn load_special_conditions_scan_sample(
smp_path: &Path,
) -> Result<RuntimeSpecialConditionsScanSample, Box<dyn std::error::Error>> {
let bytes = fs::read(smp_path)?;
let table_len = SPECIAL_CONDITION_COUNT * 4;
let table_end = SPECIAL_CONDITIONS_OFFSET
.checked_add(table_len)
.ok_or("special-conditions table overflow")?;
if bytes.len() < table_end {
return Err(format!(
"{} is too small for the fixed special-conditions table",
smp_path.display()
)
.into());
}
let hidden_sentinel = read_u32_le(
&bytes,
SPECIAL_CONDITIONS_OFFSET + SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT * 4,
)
.ok_or_else(|| {
format!(
"{} is missing the hidden special-condition sentinel",
smp_path.display()
)
})?;
if hidden_sentinel != 1 {
return Err(format!(
"{} does not match the fixed special-conditions table sentinel",
smp_path.display()
)
.into());
}
let enabled_visible_labels = (0..SPECIAL_CONDITION_HIDDEN_SENTINEL_SLOT)
.filter_map(|slot_index| {
let value = read_u32_le(&bytes, SPECIAL_CONDITIONS_OFFSET + slot_index * 4)?;
(value != 0).then(|| {
format!(
"slot {}: {}",
slot_index, SPECIAL_CONDITION_LABELS[slot_index]
)
})
})
.collect::<Vec<_>>();
let extension = smp_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
let profile_family = classify_candidate_table_header_profile(Some(extension.clone()), &bytes);
let source_kind = match extension.as_str() {
"gmp" => "map-fixed-special-conditions-range",
"gms" => "save-fixed-special-conditions-range",
"gmx" => "sandbox-fixed-special-conditions-range",
_ => "fixed-special-conditions-range",
}
.to_string();
Ok(RuntimeSpecialConditionsScanSample {
path: smp_path.display().to_string(),
profile_family,
source_kind,
enabled_visible_count: enabled_visible_labels.len(),
enabled_visible_labels,
})
}

View file

@ -0,0 +1,581 @@
use super::*;
use crate::app::runtime_compare::{
RuntimeCandidateTableEntrySample, RuntimeCandidateTableSample, RuntimeClassicProfileSample,
RuntimeRecipeBookLineSample, RuntimeRt3105ProfileSample, RuntimeSetupLaunchPayloadSample,
RuntimeSetupPayloadCoreSample, collect_numbered_candidate_name_runs,
diff_candidate_table_samples, diff_classic_profile_samples,
diff_recipe_book_line_content_samples, diff_recipe_book_line_samples,
diff_rt3_105_profile_samples, diff_setup_launch_payload_samples,
diff_setup_payload_core_samples,
};
#[test]
fn diffs_classic_profile_samples_across_multiple_files() {
let sample_a = RuntimeClassicProfileSample {
path: "a.gms".to_string(),
profile_family: "rt3-classic-save-container-v1".to_string(),
progress_32dc_offset: 0x76e8,
progress_3714_offset: 0x76ec,
progress_3715_offset: 0x77f8,
packed_profile_offset: 0x76f0,
packed_profile_len: 0x108,
packed_profile_block: SmpClassicPackedProfileBlock {
relative_len: 0x108,
relative_len_hex: "0x108".to_string(),
leading_word_0: 0x03000000,
leading_word_0_hex: "0x03000000".to_string(),
trailing_zero_word_count_after_leading_word: 3,
map_path_offset: 0x13,
map_path: Some("British Isles.gmp".to_string()),
display_name_offset: 0x46,
display_name: Some("British Isles".to_string()),
profile_byte_0x77: 0,
profile_byte_0x77_hex: "0x00".to_string(),
profile_byte_0x82: 0,
profile_byte_0x82_hex: "0x00".to_string(),
profile_byte_0x97: 0,
profile_byte_0x97_hex: "0x00".to_string(),
profile_byte_0xc5: 0,
profile_byte_0xc5_hex: "0x00".to_string(),
stable_nonzero_words: vec![SmpPackedProfileWordLane {
relative_offset: 0,
relative_offset_hex: "0x00".to_string(),
value: 0x03000000,
value_hex: "0x03000000".to_string(),
}],
},
};
let mut sample_b = sample_a.clone();
sample_b.path = "b.gms".to_string();
sample_b.packed_profile_block.leading_word_0 = 0x05000000;
sample_b.packed_profile_block.leading_word_0_hex = "0x05000000".to_string();
sample_b.packed_profile_block.stable_nonzero_words[0].value = 0x05000000;
sample_b.packed_profile_block.stable_nonzero_words[0].value_hex = "0x05000000".to_string();
let differences =
diff_classic_profile_samples(&[sample_a, sample_b]).expect("diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.packed_profile_block.leading_word_0")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.packed_profile_block.leading_word_0_hex")
);
assert!(differences.iter().any(
|entry| entry.field_path == "$.packed_profile_block.stable_nonzero_words[0].value"
));
}
#[test]
fn diffs_rt3_105_profile_samples_across_multiple_files() {
let sample_a = RuntimeRt3105ProfileSample {
path: "a.gms".to_string(),
profile_family: "rt3-105-save-container-v1".to_string(),
packed_profile_offset: 0x73c0,
packed_profile_len: 0x108,
packed_profile_block: SmpRt3105PackedProfileBlock {
relative_len: 0x108,
relative_len_hex: "0x108".to_string(),
leading_word_0: 3,
leading_word_0_hex: "0x00000003".to_string(),
trailing_zero_word_count_after_leading_word: 2,
header_flag_word_3: 0x01000000,
header_flag_word_3_hex: "0x01000000".to_string(),
map_path_offset: 0x10,
map_path: Some("Alternate USA.gmp".to_string()),
display_name_offset: 0x43,
display_name: Some("Alternate USA".to_string()),
profile_byte_0x77: 0x07,
profile_byte_0x77_hex: "0x07".to_string(),
profile_byte_0x82: 0x4d,
profile_byte_0x82_hex: "0x4d".to_string(),
profile_byte_0x97: 0x00,
profile_byte_0x97_hex: "0x00".to_string(),
profile_byte_0xc5: 0x00,
profile_byte_0xc5_hex: "0x00".to_string(),
stable_nonzero_words: vec![SmpPackedProfileWordLane {
relative_offset: 0x80,
relative_offset_hex: "0x80".to_string(),
value: 0x364d0000,
value_hex: "0x364d0000".to_string(),
}],
},
};
let mut sample_b = sample_a.clone();
sample_b.path = "b.gms".to_string();
sample_b.profile_family = "rt3-105-alt-save-container-v1".to_string();
sample_b.packed_profile_block.map_path = Some("Southern Pacific.gmp".to_string());
sample_b.packed_profile_block.display_name = Some("Southern Pacific".to_string());
sample_b.packed_profile_block.leading_word_0 = 5;
sample_b.packed_profile_block.leading_word_0_hex = "0x00000005".to_string();
sample_b.packed_profile_block.profile_byte_0x82 = 0x90;
sample_b.packed_profile_block.profile_byte_0x82_hex = "0x90".to_string();
sample_b.packed_profile_block.stable_nonzero_words[0].value = 0x1b900000;
sample_b.packed_profile_block.stable_nonzero_words[0].value_hex = "0x1b900000".to_string();
let differences =
diff_rt3_105_profile_samples(&[sample_a, sample_b]).expect("diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.profile_family")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.packed_profile_block.map_path")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.packed_profile_block.profile_byte_0x82")
);
}
#[test]
fn diffs_candidate_table_samples_across_multiple_files() {
let mut availability_a = BTreeMap::new();
availability_a.insert("AutoPlant".to_string(), 1u32);
availability_a.insert("Nuclear Power Plant".to_string(), 0u32);
let sample_a = RuntimeCandidateTableSample {
path: "a.gmp".to_string(),
profile_family: "rt3-105-map-container-v1".to_string(),
source_kind: "map-fixed-catalog-range".to_string(),
semantic_family: "scenario-named-candidate-availability-table".to_string(),
header_word_0_hex: "0x10000000".to_string(),
header_word_1_hex: "0x00009000".to_string(),
header_word_2_hex: "0x0000332e".to_string(),
observed_entry_count: 67,
zero_trailer_entry_count: 1,
nonzero_trailer_entry_count: 66,
zero_trailer_entry_names: vec!["Nuclear Power Plant".to_string()],
footer_progress_word_0_hex: "0x000032dc".to_string(),
footer_progress_word_1_hex: "0x00003714".to_string(),
availability_by_name: availability_a,
};
let mut availability_b = BTreeMap::new();
availability_b.insert("AutoPlant".to_string(), 0u32);
availability_b.insert("Nuclear Power Plant".to_string(), 0u32);
let sample_b = RuntimeCandidateTableSample {
path: "b.gmp".to_string(),
profile_family: "rt3-105-scenario-map-container-v1".to_string(),
source_kind: "map-fixed-catalog-range".to_string(),
semantic_family: "scenario-named-candidate-availability-table".to_string(),
header_word_0_hex: "0x00000000".to_string(),
header_word_1_hex: "0x00000000".to_string(),
header_word_2_hex: "0x0000332e".to_string(),
observed_entry_count: 67,
zero_trailer_entry_count: 2,
nonzero_trailer_entry_count: 65,
zero_trailer_entry_names: vec!["AutoPlant".to_string(), "Nuclear Power Plant".to_string()],
footer_progress_word_0_hex: "0x000032dc".to_string(),
footer_progress_word_1_hex: "0x00003714".to_string(),
availability_by_name: availability_b,
};
let differences =
diff_candidate_table_samples(&[sample_a, sample_b]).expect("diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.profile_family")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.header_word_0_hex")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.availability_by_name.AutoPlant")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.zero_trailer_entry_names[0]")
);
}
#[test]
fn collects_numbered_candidate_name_runs_by_prefix() {
let entries = vec![
RuntimeCandidateTableEntrySample {
index: 35,
offset: 28535,
text: "Port00".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 43,
offset: 28807,
text: "Warehouse00".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 45,
offset: 28875,
text: "Port01".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 46,
offset: 28909,
text: "Port02".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 56,
offset: 29249,
text: "Warehouse01".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
RuntimeCandidateTableEntrySample {
index: 57,
offset: 29283,
text: "Warehouse02".to_string(),
availability_dword: 1,
availability_dword_hex: "0x00000001".to_string(),
trailer_word: 1,
trailer_word_hex: "0x00000001".to_string(),
},
];
let port_runs = collect_numbered_candidate_name_runs(&entries, "Port");
let warehouse_runs = collect_numbered_candidate_name_runs(&entries, "Warehouse");
assert_eq!(port_runs.len(), 2);
assert_eq!(port_runs[0].first_name, "Port00");
assert_eq!(port_runs[0].count, 1);
assert_eq!(port_runs[1].first_name, "Port01");
assert_eq!(port_runs[1].last_name, "Port02");
assert_eq!(port_runs[1].count, 2);
assert_eq!(warehouse_runs.len(), 2);
assert_eq!(warehouse_runs[0].first_name, "Warehouse00");
assert_eq!(warehouse_runs[0].count, 1);
assert_eq!(warehouse_runs[1].first_name, "Warehouse01");
assert_eq!(warehouse_runs[1].last_name, "Warehouse02");
assert_eq!(warehouse_runs[1].count, 2);
}
#[test]
fn diffs_recipe_book_line_samples_across_multiple_files() {
let sample_a = RuntimeRecipeBookLineSample {
path: "a.gmp".to_string(),
profile_family: "rt3-105-map-container-v1".to_string(),
source_kind: "recipe-book-summary".to_string(),
book_count: 12,
book_stride_hex: "0x4e1".to_string(),
line_count: 5,
line_stride_hex: "0x30".to_string(),
book_head_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
book_line_area_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
max_annual_production_word_hex_by_book: BTreeMap::from([(
"book00".to_string(),
"0x41200000".to_string(),
)]),
line_kind_by_path: BTreeMap::from([("book00.line00".to_string(), "mixed".to_string())]),
mode_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000003".to_string(),
)]),
annual_amount_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x41a00000".to_string(),
)]),
supplied_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000017".to_string(),
)]),
demanded_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x0000002a".to_string(),
)]),
};
let sample_b = RuntimeRecipeBookLineSample {
path: "b.gms".to_string(),
profile_family: "rt3-105-alt-save-container-v1".to_string(),
source_kind: "recipe-book-summary".to_string(),
book_count: 12,
book_stride_hex: "0x4e1".to_string(),
line_count: 5,
line_stride_hex: "0x30".to_string(),
book_head_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
book_line_area_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
max_annual_production_word_hex_by_book: BTreeMap::from([(
"book00".to_string(),
"0x41200000".to_string(),
)]),
line_kind_by_path: BTreeMap::from([("book00.line00".to_string(), "zero".to_string())]),
mode_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000000".to_string(),
)]),
annual_amount_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000000".to_string(),
)]),
supplied_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000000".to_string(),
)]),
demanded_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line00".to_string(),
"0x00000000".to_string(),
)]),
};
let differences = diff_recipe_book_line_samples(&[sample_a, sample_b])
.expect("recipe-book diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.profile_family")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.line_kind_by_path.book00.line00")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.mode_word_hex_by_path.book00.line00")
);
assert!(
differences.iter().any(
|entry| entry.field_path == "$.supplied_cargo_token_word_hex_by_path.book00.line00"
)
);
}
#[test]
fn recipe_book_content_diff_ignores_wrapper_metadata() {
let sample_a = RuntimeRecipeBookLineSample {
path: "a.gmp".to_string(),
profile_family: "rt3-105-map-container-v1".to_string(),
source_kind: "recipe-book-summary".to_string(),
book_count: 12,
book_stride_hex: "0x4e1".to_string(),
line_count: 5,
line_stride_hex: "0x30".to_string(),
book_head_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
book_line_area_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
max_annual_production_word_hex_by_book: BTreeMap::from([(
"book00".to_string(),
"0x00000000".to_string(),
)]),
line_kind_by_path: BTreeMap::from([("book00.line02".to_string(), "mixed".to_string())]),
mode_word_hex_by_path: BTreeMap::from([(
"book00.line02".to_string(),
"0x00110000".to_string(),
)]),
annual_amount_word_hex_by_path: BTreeMap::from([(
"book00.line02".to_string(),
"0x00000000".to_string(),
)]),
supplied_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line02".to_string(),
"0x000040a0".to_string(),
)]),
demanded_cargo_token_word_hex_by_path: BTreeMap::from([(
"book00.line01".to_string(),
"0x72470000".to_string(),
)]),
};
let mut sample_b = sample_a.clone();
sample_b.path = "b.gms".to_string();
sample_b.profile_family = "rt3-105-save-container-v1".to_string();
sample_b.source_kind = "recipe-book-summary".to_string();
let differences = diff_recipe_book_line_samples(&[sample_a.clone(), sample_b.clone()])
.expect("wrapper-aware diff should succeed");
let content_differences = diff_recipe_book_line_content_samples(&[sample_a, sample_b])
.expect("content diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.profile_family")
);
assert!(content_differences.is_empty());
}
#[test]
fn diffs_setup_payload_core_samples_across_multiple_files() {
let sample_a = RuntimeSetupPayloadCoreSample {
path: "a.gmp".to_string(),
file_extension: "gmp".to_string(),
inferred_profile_family: "rt3-105-map-container-v1".to_string(),
payload_word_0x14: 0x0001,
payload_word_0x14_hex: "0x0001".to_string(),
payload_byte_0x20: 0x05,
payload_byte_0x20_hex: "0x05".to_string(),
marker_bytes_0x2c9_0x2d0_hex: "0000000000000000".to_string(),
row_category_byte_0x31a: 0x00,
row_category_byte_0x31a_hex: "0x00".to_string(),
row_visibility_byte_0x31b: 0x00,
row_visibility_byte_0x31b_hex: "0x00".to_string(),
row_visibility_byte_0x31c: 0x00,
row_visibility_byte_0x31c_hex: "0x00".to_string(),
row_count_word_0x3ae: 0x0186,
row_count_word_0x3ae_hex: "0x0186".to_string(),
payload_word_0x3b2: 0x0001,
payload_word_0x3b2_hex: "0x0001".to_string(),
payload_word_0x3ba: 0x0001,
payload_word_0x3ba_hex: "0x0001".to_string(),
candidate_header_word_0_hex: Some("0x10000000".to_string()),
candidate_header_word_1_hex: Some("0x00009000".to_string()),
};
let sample_b = RuntimeSetupPayloadCoreSample {
path: "b.gms".to_string(),
file_extension: "gms".to_string(),
inferred_profile_family: "rt3-105-scenario-save-container-v1".to_string(),
payload_word_0x14: 0x0001,
payload_word_0x14_hex: "0x0001".to_string(),
payload_byte_0x20: 0x05,
payload_byte_0x20_hex: "0x05".to_string(),
marker_bytes_0x2c9_0x2d0_hex: "0000000000000000".to_string(),
row_category_byte_0x31a: 0x00,
row_category_byte_0x31a_hex: "0x00".to_string(),
row_visibility_byte_0x31b: 0x00,
row_visibility_byte_0x31b_hex: "0x00".to_string(),
row_visibility_byte_0x31c: 0x00,
row_visibility_byte_0x31c_hex: "0x00".to_string(),
row_count_word_0x3ae: 0x0186,
row_count_word_0x3ae_hex: "0x0186".to_string(),
payload_word_0x3b2: 0x0006,
payload_word_0x3b2_hex: "0x0006".to_string(),
payload_word_0x3ba: 0x0001,
payload_word_0x3ba_hex: "0x0001".to_string(),
candidate_header_word_0_hex: Some("0x00000000".to_string()),
candidate_header_word_1_hex: Some("0x00000000".to_string()),
};
let differences =
diff_setup_payload_core_samples(&[sample_a, sample_b]).expect("diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.file_extension")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.inferred_profile_family")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.payload_word_0x3b2")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.candidate_header_word_0_hex")
);
}
#[test]
fn diffs_setup_launch_payload_samples_across_multiple_files() {
let sample_a = RuntimeSetupLaunchPayloadSample {
path: "a.gmp".to_string(),
file_extension: "gmp".to_string(),
inferred_profile_family: "rt3-105-map-container-v1".to_string(),
launch_flag_byte_0x22: 0x53,
launch_flag_byte_0x22_hex: "0x53".to_string(),
campaign_progress_in_known_range: false,
campaign_progress_scenario_name: None,
campaign_progress_page_index: None,
launch_selector_byte_0x33: 0x00,
launch_selector_byte_0x33_hex: "0x00".to_string(),
launch_token_block_0x23_0x32_hex: "01311154010000000000000000000000".to_string(),
campaign_selector_values: BTreeMap::from([
("Go West!".to_string(), 0x01),
("Germantown".to_string(), 0x31),
]),
nonzero_campaign_selector_values: BTreeMap::from([
("Go West!".to_string(), 0x01),
("Germantown".to_string(), 0x31),
]),
};
let sample_b = RuntimeSetupLaunchPayloadSample {
path: "b.gms".to_string(),
file_extension: "gms".to_string(),
inferred_profile_family: "rt3-105-save-container-v1".to_string(),
launch_flag_byte_0x22: 0xae,
launch_flag_byte_0x22_hex: "0xae".to_string(),
campaign_progress_in_known_range: false,
campaign_progress_scenario_name: None,
campaign_progress_page_index: None,
launch_selector_byte_0x33: 0x00,
launch_selector_byte_0x33_hex: "0x00".to_string(),
launch_token_block_0x23_0x32_hex: "01439aae010000000000000000000000".to_string(),
campaign_selector_values: BTreeMap::from([
("Go West!".to_string(), 0x01),
("Germantown".to_string(), 0x43),
]),
nonzero_campaign_selector_values: BTreeMap::from([
("Go West!".to_string(), 0x01),
("Germantown".to_string(), 0x43),
]),
};
let differences =
diff_setup_launch_payload_samples(&[sample_a, sample_b]).expect("diff should succeed");
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.file_extension")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.inferred_profile_family")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.launch_flag_byte_0x22")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.launch_token_block_0x23_0x32_hex")
);
assert!(
differences
.iter()
.any(|entry| entry.field_path == "$.campaign_selector_values.Germantown")
);
}

View file

@ -0,0 +1,32 @@
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use super::finance::{diff_finance_outcomes, load_finance_outcome};
use super::helpers::state_io::{
export_runtime_overlay_import_document, export_runtime_save_slice_document,
load_normalized_runtime_state,
};
use super::runtime_fixture_state::{
diff_state, export_fixture_state, snapshot_state, summarize_fixture, summarize_state,
};
use rrt_fixtures::diff_json_values;
use rrt_model::finance::{
AnnualFinanceDecision, AnnualFinanceEvaluation, CompanyFinanceState, DebtRestructureSummary,
FinanceOutcome, FinanceSnapshot,
};
use rrt_runtime::documents::{
load_runtime_overlay_import_document, load_runtime_save_slice_document,
};
use rrt_runtime::inspect::smp::{
profiles::{
SmpClassicPackedProfileBlock, SmpPackedProfileWordLane, SmpRt3105PackedProfileBlock,
},
save_load::SmpLoadedSaveSlice,
};
mod compare;
mod state;
mod support;
use support::*;

View file

@ -0,0 +1,410 @@
use super::*;
#[test]
fn diffs_outcomes_recursively() {
let left = FinanceOutcome {
evaluation: AnnualFinanceEvaluation::no_action(),
post_company: CompanyFinanceState::default(),
};
let mut right = left.clone();
right.post_company.current_cash = 123;
right.evaluation.debt_restructure = DebtRestructureSummary {
retired_principal: 10,
issued_principal: 20,
};
let report = diff_finance_outcomes(&left, &right).expect("diff should succeed");
assert!(!report.matches);
let diff_paths: Vec<_> = report
.differences
.iter()
.map(|entry| entry.path.clone())
.collect();
assert_diff_paths_include(&diff_paths, "$.post_company.current_cash");
assert_diff_paths_include(
&diff_paths,
"$.evaluation.debt_restructure.retired_principal",
);
}
#[test]
fn diffs_runtime_states_recursively() {
let left = serde_json::json!({
"format_version": 1,
"snapshot_id": "left",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {
"sandbox": false
},
"companies": []
}
});
let right = serde_json::json!({
"format_version": 1,
"snapshot_id": "right",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 2
},
"world_flags": {
"sandbox": true
},
"companies": []
}
});
let left_path = write_temp_json("runtime-diff-left", &left);
let right_path = write_temp_json("runtime-diff-right", &right);
diff_state(&left_path, &right_path).expect("runtime diff should succeed");
let _ = fs::remove_file(left_path);
let _ = fs::remove_file(right_path);
}
#[test]
fn diffs_runtime_states_with_event_record_additions_and_removals() {
let left = serde_json::json!({
"format_version": 1,
"snapshot_id": "left-events",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {
"sandbox": false
},
"companies": [],
"event_runtime_records": [
{
"record_id": 1,
"trigger_kind": 7,
"active": true
},
{
"record_id": 2,
"trigger_kind": 7,
"active": false
}
]
}
});
let right = serde_json::json!({
"format_version": 1,
"snapshot_id": "right-events",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {
"sandbox": false
},
"companies": [],
"event_runtime_records": [
{
"record_id": 1,
"trigger_kind": 7,
"active": true
}
]
}
});
let left_path = write_temp_json("runtime-diff-events-left", &left);
let right_path = write_temp_json("runtime-diff-events-right", &right);
let left_state =
load_normalized_runtime_state(&left_path).expect("left runtime state should load");
let right_state =
load_normalized_runtime_state(&right_path).expect("right runtime state should load");
let differences = diff_json_values(&left_state, &right_state);
let diff_paths: Vec<_> = differences.iter().map(|entry| entry.path.clone()).collect();
assert_diff_paths_include(&diff_paths, "$.event_runtime_records[1]");
let _ = fs::remove_file(left_path);
let _ = fs::remove_file(right_path);
}
#[test]
fn diffs_runtime_states_with_packed_event_collection_changes() {
let left = serde_json::json!({
"format_version": 1,
"snapshot_id": "left-packed-events",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {},
"companies": [],
"packed_event_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 5,
"live_record_count": 3,
"live_entry_ids": [1, 3, 5],
"decoded_record_count": 0,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 1,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["left fixture"]
},
{
"record_index": 1,
"live_entry_id": 3,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["left fixture"]
},
{
"record_index": 2,
"live_entry_id": 5,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["left fixture"]
}
]
},
"event_runtime_records": []
}
});
let right = serde_json::json!({
"format_version": 1,
"snapshot_id": "right-packed-events",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {},
"companies": [],
"packed_event_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 5,
"live_record_count": 2,
"live_entry_ids": [1, 5],
"decoded_record_count": 0,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 1,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["right fixture"]
},
{
"record_index": 1,
"live_entry_id": 5,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["right fixture"]
}
]
},
"event_runtime_records": []
}
});
let left_path = write_temp_json("runtime-diff-packed-events-left", &left);
let right_path = write_temp_json("runtime-diff-packed-events-right", &right);
let left_state =
load_normalized_runtime_state(&left_path).expect("left runtime state should load");
let right_state =
load_normalized_runtime_state(&right_path).expect("right runtime state should load");
let differences = diff_json_values(&left_state, &right_state);
assert!(differences.iter().any(|entry| {
entry.path == "$.packed_event_collection.live_record_count"
|| entry.path == "$.packed_event_collection.live_entry_ids[1]"
}));
let _ = fs::remove_file(left_path);
let _ = fs::remove_file(right_path);
}
#[test]
fn diffs_runtime_states_with_packed_record_and_runtime_record_import_changes() {
let left = serde_json::json!({
"format_version": 1,
"snapshot_id": "left-packed-import",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 0
},
"world_flags": {},
"companies": [],
"packed_event_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 7,
"live_record_count": 1,
"live_entry_ids": [7],
"decoded_record_count": 0,
"imported_runtime_record_count": 0,
"records": [
{
"record_index": 0,
"live_entry_id": 7,
"decode_status": "unsupported_framing",
"payload_family": "unsupported_framing",
"grouped_effect_row_counts": [0, 0, 0, 0],
"decoded_actions": [],
"executable_import_ready": false,
"notes": ["left placeholder"]
}
]
},
"event_runtime_records": []
}
});
let right = serde_json::json!({
"format_version": 1,
"snapshot_id": "right-packed-import",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 0
},
"world_flags": {},
"companies": [],
"packed_event_collection": {
"source_kind": "packed-event-runtime-collection",
"mechanism_family": "classic-save-rehydrate-v1",
"mechanism_confidence": "grounded",
"container_profile_family": "rt3-classic-save-container-v1",
"packed_state_version": 1001,
"packed_state_version_hex": "0x000003e9",
"live_id_bound": 7,
"live_record_count": 1,
"live_entry_ids": [7],
"decoded_record_count": 1,
"imported_runtime_record_count": 1,
"records": [
{
"record_index": 0,
"live_entry_id": 7,
"payload_offset": 29186,
"payload_len": 64,
"decode_status": "executable",
"payload_family": "synthetic_harness",
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"text_bands": [
{
"label": "primary_text_band",
"packed_len": 5,
"present": true,
"preview": "Alpha"
}
],
"standalone_condition_row_count": 1,
"standalone_condition_rows": [],
"grouped_effect_row_counts": [0, 1, 0, 0],
"grouped_effect_rows": [],
"decoded_actions": [
{
"kind": "set_world_flag",
"key": "from_packed_root",
"value": true
}
],
"executable_import_ready": true,
"notes": ["decoded test record"]
}
]
},
"event_runtime_records": [
{
"record_id": 7,
"trigger_kind": 7,
"active": true,
"marks_collection_dirty": false,
"one_shot": false,
"has_fired": false,
"effects": [
{
"kind": "set_world_flag",
"key": "from_packed_root",
"value": true
}
]
}
]
}
});
let left_path = write_temp_json("runtime-diff-packed-import-left", &left);
let right_path = write_temp_json("runtime-diff-packed-import-right", &right);
let left_state =
load_normalized_runtime_state(&left_path).expect("left runtime state should load");
let right_state =
load_normalized_runtime_state(&right_path).expect("right runtime state should load");
let differences = diff_json_values(&left_state, &right_state);
let diff_paths: Vec<_> = differences.iter().map(|entry| entry.path.clone()).collect();
assert!(differences.iter().any(|entry| {
entry.path == "$.packed_event_collection.records[0].decode_status"
|| entry.path == "$.packed_event_collection.records[0].decoded_actions[0]"
}));
assert_diff_paths_include(&diff_paths, "$.event_runtime_records[0]");
let _ = fs::remove_file(left_path);
let _ = fs::remove_file(right_path);
}

View file

@ -0,0 +1,176 @@
use super::*;
#[test]
fn exports_and_summarizes_runtime_snapshot() {
let fixture = serde_json::json!({
"format_version": 1,
"fixture_id": "runtime-export-test",
"source": { "kind": "synthetic" },
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 0
},
"world_flags": {},
"companies": [],
"event_runtime_records": []
},
"commands": [
{
"kind": "step_count",
"steps": 2
}
],
"expected_summary": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 2
},
"world_flag_count": 0,
"company_count": 0,
"event_runtime_record_count": 0,
"total_company_cash": 0
}
});
let fixture_path = write_temp_json("runtime-export-fixture", &fixture);
let snapshot_path = std::env::temp_dir().join(format!(
"rrt-cli-runtime-export-{}.json",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos()
));
export_fixture_state(&fixture_path, &snapshot_path).expect("fixture export should succeed");
summarize_state(&snapshot_path).expect("snapshot summary should succeed");
let _ = fs::remove_file(fixture_path);
let _ = fs::remove_file(snapshot_path);
}
#[test]
fn snapshots_runtime_state_input_into_snapshot() {
let input = serde_json::json!({
"format_version": 1,
"input_id": "runtime-input-test",
"source": {
"description": "test runtime state input"
},
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 9
},
"world_flags": {},
"companies": [],
"event_runtime_records": [],
"service_state": {
"periodic_boundary_calls": 0,
"trigger_dispatch_counts": {},
"total_event_record_services": 0,
"dirty_rerun_count": 0
}
}
});
let input_path = write_temp_json("runtime-input", &input);
let output_path = std::env::temp_dir().join(format!(
"rrt-cli-runtime-input-{}.json",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos()
));
snapshot_state(&input_path, &output_path).expect("runtime snapshot should succeed");
summarize_state(&output_path).expect("snapshotted output should summarize");
let _ = fs::remove_file(input_path);
let _ = fs::remove_file(output_path);
}
#[test]
fn exports_runtime_save_slice_document_from_loaded_slice() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let output_path = std::env::temp_dir().join(format!("rrt-export-save-slice-test-{nonce}.json"));
let smp_path = PathBuf::from("captured-test.gms");
let report = export_runtime_save_slice_document(
&smp_path,
&output_path,
SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
cargo_catalog: None,
world_issue_37_state: None,
world_economic_tuning_state: None,
world_finance_neighborhood_state: None,
world_locomotive_policy_state: None,
company_roster: None,
chairman_profile_table: None,
region_collection: None,
region_fixed_row_run_summary: None,
placed_structure_collection: None,
placed_structure_dynamic_side_buffer_summary: None,
special_conditions_table: None,
event_runtime_collection: None,
notes: vec!["exported for test".to_string()],
},
)
.expect("save slice export should succeed");
assert_eq!(report.save_slice_id, "captured-test");
let document = load_runtime_save_slice_document(&output_path)
.expect("exported save slice document should load");
assert_eq!(document.save_slice_id, "captured-test");
assert_eq!(
document.source.original_save_filename.as_deref(),
Some("captured-test.gms")
);
let _ = fs::remove_file(output_path);
}
#[test]
fn exports_runtime_overlay_import_document() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let output_path =
std::env::temp_dir().join(format!("rrt-export-overlay-import-test-{nonce}.json"));
let snapshot_path = PathBuf::from("base-snapshot.json");
let save_slice_path = PathBuf::from("captured-save-slice.json");
let report =
export_runtime_overlay_import_document(&snapshot_path, &save_slice_path, &output_path)
.expect("overlay import export should succeed");
let expected_import_id = output_path
.file_stem()
.and_then(|stem| stem.to_str())
.expect("output path should have a stem")
.to_string();
assert_eq!(report.import_id, expected_import_id);
let document = load_runtime_overlay_import_document(&output_path)
.expect("exported overlay import document should load");
assert_eq!(document.import_id, expected_import_id);
assert_eq!(document.base_snapshot_path, "base-snapshot.json");
assert_eq!(document.save_slice_path, "captured-save-slice.json");
let _ = fs::remove_file(output_path);
}

View file

@ -0,0 +1,77 @@
use super::*;
#[test]
fn summarizes_runtime_fixture() {
let fixture = serde_json::json!({
"format_version": 1,
"fixture_id": "runtime-fixture-test",
"source": { "kind": "synthetic" },
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 0
},
"world_flags": {
"sandbox": false
},
"companies": [],
"event_runtime_records": []
},
"commands": [
{
"kind": "advance_to",
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 3
}
}
],
"expected_summary": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 3
},
"world_flag_count": 1,
"company_count": 0,
"event_runtime_record_count": 0,
"total_company_cash": 0
},
"expected_state_fragment": {
"calendar": {
"tick_slot": 3
},
"world_flags": {
"sandbox": false
}
}
});
let path = write_temp_json("runtime-fixture", &fixture);
summarize_fixture(&path).expect("fixture summary should succeed");
let _ = fs::remove_file(path);
}
#[test]
fn summarizes_snapshot_backed_fixture_with_packed_event_collection() {
let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-collection-from-snapshot.json");
summarize_fixture(&fixture_path)
.expect("snapshot-backed packed-event fixture should summarize");
}
#[test]
fn summarizes_snapshot_backed_fixture_with_imported_packed_event_record() {
let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-record-import-from-snapshot.json");
summarize_fixture(&fixture_path)
.expect("snapshot-backed imported packed-event fixture should summarize");
}

View file

@ -0,0 +1,7 @@
use super::*;
mod diff;
mod document_io;
mod fixture_summary;
mod save_slice_overlay;
mod snapshot_io;

View file

@ -0,0 +1,265 @@
use super::*;
#[test]
fn summarizes_save_slice_backed_fixtures() {
let parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-parity-save-slice-fixture.json");
let selective_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-save-slice-fixture.json");
let overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-overlay-fixture.json");
let symbolic_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-symbolic-company-scope-overlay-fixture.json");
let negative_company_scope_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-negative-company-scope-overlay-fixture.json");
let deactivate_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-company-overlay-fixture.json");
let track_capacity_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-track-capacity-overlay-fixture.json");
let mixed_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-mixed-company-descriptor-overlay-fixture.json");
let named_locomotive_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-named-locomotive-availability-save-slice-fixture.json",
);
let missing_catalog_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-locomotive-availability-missing-catalog-save-slice-fixture.json",
);
let save_locomotive_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-locomotive-availability-save-slice-fixture.json",
);
let overlay_locomotive_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-locomotive-availability-overlay-fixture.json");
let save_locomotive_cost_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-locomotive-cost-save-slice-fixture.json");
let overlay_locomotive_cost_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-locomotive-cost-overlay-fixture.json");
let scalar_band_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-band-parity-save-slice-fixture.json",
);
let world_scalar_executable_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-executable-save-slice-fixture.json",
);
let world_scalar_override_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-world-scalar-override-save-slice-fixture.json");
let runtime_variable_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-runtime-variable-overlay-fixture.json");
let runtime_variable_condition_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(
"../../fixtures/runtime/packed-event-runtime-variable-condition-overlay-fixture.json",
);
let cargo_economics_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-cargo-economics-save-slice-fixture.json");
let cargo_economics_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-cargo-economics-parity-save-slice-fixture.json");
let add_building_shell_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-add-building-shell-save-slice-fixture.json");
let world_scalar_condition_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-world-scalar-condition-save-slice-fixture.json");
let world_scalar_condition_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-world-scalar-condition-parity-save-slice-fixture.json",
);
let cargo_catalog_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-cargo-catalog-save-slice-fixture.json");
let chairman_cash_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-cash-overlay-fixture.json");
let chairman_cash_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-cash-save-slice-fixture.json");
let chairman_condition_true_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-chairman-condition-true-save-slice-fixture.json",
);
let chairman_human_cash_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-human-cash-save-slice-fixture.json");
let deactivate_chairman_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-chairman-overlay-fixture.json");
let deactivate_chairman_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-chairman-save-slice-fixture.json");
let deactivate_chairman_ai_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-chairman-ai-save-slice-fixture.json");
let deactivate_company_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-deactivate-company-save-slice-fixture.json");
let track_capacity_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-track-capacity-save-slice-fixture.json");
let negative_company_scope_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-negative-company-scope-save-slice-fixture.json");
let missing_chairman_context_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-chairman-missing-context-save-slice-fixture.json",
);
let chairman_scope_parity_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-scope-parity-save-slice-fixture.json");
let chairman_condition_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-condition-overlay-fixture.json");
let chairman_condition_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-chairman-condition-save-slice-fixture.json");
let company_governance_condition_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(
"../../fixtures/runtime/packed-event-company-governance-condition-overlay-fixture.json",
);
let company_governance_condition_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-company-governance-condition-save-slice-fixture.json",
);
let selection_only_context_overlay_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selection-only-context-overlay-fixture.json");
let credit_rating_descriptor_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-credit-rating-descriptor-save-slice-fixture.json",
);
let stock_prices_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-stock-prices-shell-save-slice-fixture.json");
let game_won_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-game-won-shell-save-slice-fixture.json");
let merger_premium_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-merger-premium-shell-save-slice-fixture.json");
let set_human_control_shell_save_fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/runtime/packed-event-set-human-control-shell-save-slice-fixture.json",
);
let investor_confidence_condition_save_fixture = PathBuf::from(env!(
"CARGO_MANIFEST_DIR"
))
.join(
"../../fixtures/runtime/packed-event-investor-confidence-condition-save-slice-fixture.json",
);
let management_attitude_condition_save_fixture = PathBuf::from(env!(
"CARGO_MANIFEST_DIR"
))
.join(
"../../fixtures/runtime/packed-event-management-attitude-condition-save-slice-fixture.json",
);
summarize_fixture(&parity_fixture).expect("save-slice-backed parity fixture should summarize");
summarize_fixture(&selective_fixture)
.expect("save-slice-backed selective-import fixture should summarize");
summarize_fixture(&overlay_fixture)
.expect("overlay-backed selective-import fixture should summarize");
summarize_fixture(&symbolic_overlay_fixture)
.expect("overlay-backed symbolic-target fixture should summarize");
summarize_fixture(&negative_company_scope_overlay_fixture)
.expect("overlay-backed negative-sentinel company-scope fixture should summarize");
summarize_fixture(&deactivate_overlay_fixture)
.expect("overlay-backed deactivate-company fixture should summarize");
summarize_fixture(&track_capacity_overlay_fixture)
.expect("overlay-backed track-capacity fixture should summarize");
summarize_fixture(&mixed_overlay_fixture)
.expect("overlay-backed mixed real-row fixture should summarize");
summarize_fixture(&named_locomotive_fixture)
.expect("save-slice-backed named locomotive availability fixture should summarize");
summarize_fixture(&missing_catalog_fixture).expect(
"save-slice-backed locomotive availability missing-catalog fixture should summarize",
);
summarize_fixture(&save_locomotive_fixture)
.expect("save-slice-backed locomotive availability descriptor fixture should summarize");
summarize_fixture(&overlay_locomotive_fixture)
.expect("overlay-backed locomotive availability fixture should summarize");
summarize_fixture(&save_locomotive_cost_fixture)
.expect("save-slice-backed locomotive cost fixture should summarize");
summarize_fixture(&overlay_locomotive_cost_fixture)
.expect("overlay-backed locomotive cost fixture should summarize");
summarize_fixture(&scalar_band_parity_fixture)
.expect("save-slice-backed recovered scalar-band parity fixture should summarize");
summarize_fixture(&world_scalar_executable_fixture)
.expect("save-slice-backed executable world-scalar fixture should summarize");
summarize_fixture(&world_scalar_override_fixture)
.expect("save-slice-backed world-scalar override fixture should summarize");
summarize_fixture(&runtime_variable_overlay_fixture)
.expect("overlay-backed runtime-variable fixture should summarize");
summarize_fixture(&runtime_variable_condition_overlay_fixture)
.expect("overlay-backed runtime-variable condition fixture should summarize");
summarize_fixture(&cargo_economics_fixture)
.expect("save-slice-backed cargo-economics fixture should summarize");
summarize_fixture(&cargo_economics_parity_fixture)
.expect("save-slice-backed cargo-economics parity fixture should summarize");
summarize_fixture(&add_building_shell_fixture)
.expect("save-slice-backed add-building shell fixture should summarize");
summarize_fixture(&world_scalar_condition_fixture)
.expect("save-slice-backed executable world-scalar condition fixture should summarize");
summarize_fixture(&world_scalar_condition_parity_fixture)
.expect("save-slice-backed parity world-scalar condition fixture should summarize");
summarize_fixture(&cargo_catalog_fixture)
.expect("save-slice-backed cargo catalog fixture should summarize");
summarize_fixture(&chairman_cash_overlay_fixture)
.expect("overlay-backed chairman-cash fixture should summarize");
summarize_fixture(&chairman_cash_save_fixture)
.expect("save-slice-backed chairman-cash fixture should summarize");
summarize_fixture(&chairman_condition_true_save_fixture)
.expect("save-slice-backed condition-true chairman fixture should summarize");
summarize_fixture(&chairman_human_cash_save_fixture)
.expect("save-slice-backed human-chairman cash fixture should summarize");
summarize_fixture(&deactivate_chairman_overlay_fixture)
.expect("overlay-backed deactivate-chairman fixture should summarize");
summarize_fixture(&deactivate_chairman_save_fixture)
.expect("save-slice-backed deactivate-chairman fixture should summarize");
summarize_fixture(&deactivate_chairman_ai_save_fixture)
.expect("save-slice-backed AI-chairman deactivate fixture should summarize");
summarize_fixture(&deactivate_company_save_fixture)
.expect("save-slice-backed deactivate-company fixture should summarize");
summarize_fixture(&track_capacity_save_fixture)
.expect("save-slice-backed track-capacity fixture should summarize");
summarize_fixture(&negative_company_scope_save_fixture)
.expect("save-slice-backed negative-sentinel company-scope fixture should summarize");
summarize_fixture(&missing_chairman_context_fixture)
.expect("save-slice-backed chairman missing-context fixture should summarize");
summarize_fixture(&chairman_scope_parity_fixture)
.expect("save-slice-backed chairman scope parity fixture should summarize");
summarize_fixture(&chairman_condition_overlay_fixture)
.expect("overlay-backed chairman condition fixture should summarize");
summarize_fixture(&chairman_condition_save_fixture)
.expect("save-slice-backed chairman condition fixture should summarize");
summarize_fixture(&company_governance_condition_overlay_fixture)
.expect("overlay-backed company governance condition fixture should summarize");
summarize_fixture(&company_governance_condition_save_fixture)
.expect("save-slice-backed company governance condition fixture should summarize");
summarize_fixture(&selection_only_context_overlay_fixture)
.expect("overlay-backed selection-only save context fixture should summarize");
summarize_fixture(&credit_rating_descriptor_save_fixture)
.expect("save-slice-backed credit-rating descriptor fixture should summarize");
summarize_fixture(&stock_prices_shell_save_fixture)
.expect("save-slice-backed shell-owned stock-prices fixture should summarize");
summarize_fixture(&game_won_shell_save_fixture)
.expect("save-slice-backed shell-owned game-won fixture should summarize");
summarize_fixture(&merger_premium_shell_save_fixture)
.expect("save-slice-backed shell-owned merger-premium fixture should summarize");
summarize_fixture(&set_human_control_shell_save_fixture)
.expect("save-slice-backed shell-owned set-human-control fixture should summarize");
summarize_fixture(&investor_confidence_condition_save_fixture)
.expect("save-slice-backed investor-confidence condition fixture should summarize");
summarize_fixture(&management_attitude_condition_save_fixture)
.expect("save-slice-backed management-attitude condition fixture should summarize");
}
#[test]
fn diffs_runtime_states_between_save_slice_and_overlay_import() {
let base = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-save-slice.json");
let overlay = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-overlay.json");
let left_state =
load_normalized_runtime_state(&base).expect("save-slice-backed state should load");
let right_state =
load_normalized_runtime_state(&overlay).expect("overlay-backed state should load");
let differences = diff_json_values(&left_state, &right_state);
assert!(differences.iter().any(|entry| {
entry.path == "$.companies[0].company_id"
|| entry.path == "$.packed_event_collection.imported_runtime_record_count"
|| entry.path == "$.packed_event_collection.records[1].import_outcome"
|| entry.path == "$.event_runtime_records[1].record_id"
}));
}
#[test]
fn diffs_save_slice_backed_states_across_packed_event_boundaries() {
let left_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-parity-save-slice.json");
let right_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/runtime/packed-event-selective-import-save-slice.json");
let left_state = load_normalized_runtime_state(&left_path)
.expect("left save-slice-backed state should load");
let right_state = load_normalized_runtime_state(&right_path)
.expect("right save-slice-backed state should load");
let differences = diff_json_values(&left_state, &right_state);
assert!(differences.iter().any(|entry| {
entry.path == "$.packed_event_collection.imported_runtime_record_count"
|| entry.path == "$.packed_event_collection.records[0].decode_status"
}));
}

View file

@ -0,0 +1,18 @@
use super::*;
#[test]
fn loads_snapshot_as_outcome() {
let snapshot = FinanceSnapshot {
policy: rrt_model::finance::AnnualFinancePolicy {
dividends_allowed: false,
..rrt_model::finance::AnnualFinancePolicy::default()
},
company: CompanyFinanceState::default(),
};
let path = write_temp_json("snapshot", &snapshot);
let outcome = load_finance_outcome(&path).expect("snapshot should load");
assert_eq!(outcome.evaluation.decision, AnnualFinanceDecision::NoAction);
let _ = fs::remove_file(path);
}

View file

@ -0,0 +1,6 @@
pub(crate) fn assert_diff_paths_include(paths: &[String], needle: &str) {
assert!(
paths.iter().any(|path| path == needle),
"expected diff paths to include {needle}, got {paths:?}"
);
}

View file

@ -0,0 +1,5 @@
mod json;
mod temp_files;
pub(crate) use json::*;
pub(crate) use temp_files::*;

View file

@ -0,0 +1,15 @@
use std::fs;
use std::path::PathBuf;
use serde::Serialize;
pub(crate) fn write_temp_json<T: Serialize>(stem: &str, value: &T) -> PathBuf {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!("rrt-cli-{stem}-{nonce}.json"));
let bytes = serde_json::to_vec_pretty(value).expect("json serialization should succeed");
fs::write(&path, bytes).expect("temp json should be written");
path
}

View file

@ -0,0 +1,119 @@
use std::collections::BTreeSet;
use std::fs;
use std::io::Read;
use std::path::Path;
use rrt_model::{
BINARY_SUMMARY_PATH, CANONICAL_EXE_PATH, CONTROL_LOOP_ATLAS_PATH, FUNCTION_MAP_PATH,
REQUIRED_ATLAS_HEADINGS, REQUIRED_EXPORTS, load_binary_summary, load_function_map,
};
use sha2::{Digest, Sha256};
pub(crate) fn validate_required_files(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut missing = Vec::new();
for relative in REQUIRED_EXPORTS {
let path = repo_root.join(relative);
if !path.exists() {
missing.push(path.display().to_string());
}
}
if !missing.is_empty() {
return Err(format!("missing required exports: {}", missing.join(", ")).into());
}
Ok(())
}
pub(crate) fn validate_binary_summary(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
let summary = load_binary_summary(&repo_root.join(BINARY_SUMMARY_PATH))?;
let actual_exe = repo_root.join(CANONICAL_EXE_PATH);
if !actual_exe.exists() {
return Err(format!("canonical exe missing: {}", actual_exe.display()).into());
}
let actual_hash = sha256_file(&actual_exe)?;
if actual_hash != summary.sha256 {
return Err(format!(
"hash mismatch for {}: summary has {}, actual file is {}",
actual_exe.display(),
summary.sha256,
actual_hash
)
.into());
}
let docs_readme = fs::read_to_string(repo_root.join("docs/README.md"))?;
if !docs_readme.contains(&summary.sha256) {
return Err("docs/README.md does not include the canonical SHA-256".into());
}
Ok(())
}
pub(crate) fn validate_function_map(repo_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
let records = load_function_map(&repo_root.join(FUNCTION_MAP_PATH))?;
let mut seen = BTreeSet::new();
for record in records {
if !(1..=5).contains(&record.confidence) {
return Err(format!(
"invalid confidence {} for {} {}",
record.confidence, record.address, record.name
)
.into());
}
if !seen.insert(record.address) {
return Err(format!("duplicate function address {}", record.address).into());
}
if record.name.trim().is_empty() {
return Err(format!("blank function name at {}", record.address).into());
}
}
Ok(())
}
pub(crate) fn validate_control_loop_atlas(
repo_root: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let atlas = fs::read_to_string(repo_root.join(CONTROL_LOOP_ATLAS_PATH))?;
for heading in REQUIRED_ATLAS_HEADINGS {
if !atlas.contains(heading) {
return Err(format!("missing atlas heading `{heading}`").into());
}
}
for marker in [
"- Roots:",
"- Trigger/Cadence:",
"- Key Dispatchers:",
"- State Anchors:",
"- Subsystem Handoffs:",
"- Evidence:",
"- Open Questions:",
] {
if !atlas.contains(marker) {
return Err(format!("atlas is missing field marker `{marker}`").into());
}
}
Ok(())
}
fn sha256_file(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
let mut file = fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0_u8; 8192];
loop {
let read = file.read(&mut buffer)?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
Ok(format!("{:x}", hasher.finalize()))
}

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,8 @@ pub mod diff;
pub mod load;
pub mod normalize;
pub mod schema;
pub mod summary;
pub mod validation;
pub use diff::{JsonDiffEntry, diff_json_values};
pub use load::{load_fixture_document, load_fixture_document_from_str};

View file

@ -1,9 +1,40 @@
use std::path::{Path, PathBuf};
use rrt_runtime::{
load_runtime_save_slice_document, load_runtime_snapshot_document, load_runtime_state_import,
project_save_slice_to_runtime_state_import, validate_runtime_save_slice_document,
validate_runtime_snapshot_document,
use rrt_runtime::documents::{
build_runtime_state_input_from_save_slice, load_runtime_save_slice_document,
load_runtime_state_input,
};
use rrt_runtime::persistence::{
load_runtime_snapshot_document, validate_runtime_snapshot_document,
};
#[cfg(test)]
use rrt_runtime::persistence::{
RuntimeSnapshotDocument, RuntimeSnapshotSource, SNAPSHOT_FORMAT_VERSION,
save_runtime_snapshot_document,
};
use rrt_runtime::validation::validate_runtime_save_slice_document;
#[cfg(test)]
use rrt_runtime::documents::{
OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
RuntimeOverlayImportDocumentSource, RuntimeSaveSliceDocument, RuntimeSaveSliceDocumentSource,
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, save_runtime_overlay_import_document,
save_runtime_save_slice_document,
};
#[cfg(test)]
use rrt_runtime::event::effects::RuntimeEffect;
#[cfg(test)]
use rrt_runtime::event::targets::RuntimeCompanyTarget;
#[cfg(test)]
use rrt_runtime::inspect::smp::{
events::{SmpLoadedEventRuntimeCollectionSummary, SmpLoadedPackedEventRecordSummary},
save_load::SmpLoadedSaveSlice,
};
#[cfg(test)]
use rrt_runtime::state::{
CalendarPoint, RuntimeCompany, RuntimeCompanyControllerKind, RuntimeSaveProfileState,
RuntimeServiceState, RuntimeState, RuntimeTrackPieceCounts, RuntimeWorldRestoreState,
};
use crate::{FixtureDocument, FixtureStateOrigin, RawFixtureDocument};
@ -35,10 +66,10 @@ fn resolve_raw_fixture_document(
let specified_state_inputs = usize::from(raw.state.is_some())
+ usize::from(raw.state_snapshot_path.is_some())
+ usize::from(raw.state_save_slice_path.is_some())
+ usize::from(raw.state_import_path.is_some());
+ usize::from(raw.state_input_path.is_some());
if specified_state_inputs != 1 {
return Err(
"fixture must specify exactly one of inline state, state_snapshot_path, state_save_slice_path, or state_import_path"
"fixture must specify exactly one of inline state, state_snapshot_path, state_save_slice_path, or state_input_path"
.into(),
);
}
@ -47,7 +78,7 @@ fn resolve_raw_fixture_document(
&raw.state,
&raw.state_snapshot_path,
&raw.state_save_slice_path,
&raw.state_import_path,
&raw.state_input_path,
) {
(Some(state), None, None, None) => state.clone(),
(None, Some(snapshot_path), None, None) => {
@ -70,7 +101,7 @@ fn resolve_raw_fixture_document(
save_slice_path.display()
)
})?;
project_save_slice_to_runtime_state_import(
build_runtime_state_input_from_save_slice(
&document.save_slice,
&document.save_slice_id,
document.source.description.clone(),
@ -83,13 +114,13 @@ fn resolve_raw_fixture_document(
})?
.state
}
(None, None, None, Some(import_path)) => {
let import_path = resolve_snapshot_path(base_dir, import_path);
load_runtime_state_import(&import_path)
(None, None, None, Some(input_path)) => {
let input_path = resolve_snapshot_path(base_dir, input_path);
load_runtime_state_input(&input_path)
.map_err(|err| {
format!(
"failed to load runtime import {}: {err}",
import_path.display()
"failed to load runtime input {}: {err}",
input_path.display()
)
})?
.state
@ -100,11 +131,11 @@ fn resolve_raw_fixture_document(
let state_origin = match (
raw.state_snapshot_path,
raw.state_save_slice_path,
raw.state_import_path,
raw.state_input_path,
) {
(Some(snapshot_path), None, None) => FixtureStateOrigin::SnapshotPath(snapshot_path),
(None, Some(save_slice_path), None) => FixtureStateOrigin::SaveSlicePath(save_slice_path),
(None, None, Some(import_path)) => FixtureStateOrigin::ImportPath(import_path),
(None, None, Some(input_path)) => FixtureStateOrigin::InputPath(input_path),
_ => FixtureStateOrigin::Inline,
};
@ -133,15 +164,6 @@ fn resolve_snapshot_path(base_dir: &Path, snapshot_path: &str) -> PathBuf {
mod tests {
use super::*;
use crate::FixtureStateOrigin;
use rrt_runtime::{
CalendarPoint, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
RuntimeOverlayImportDocumentSource, RuntimeSaveProfileState, RuntimeSaveSliceDocument,
RuntimeSaveSliceDocumentSource, RuntimeServiceState, RuntimeSnapshotDocument,
RuntimeSnapshotSource, RuntimeState, RuntimeTrackPieceCounts, RuntimeWorldRestoreState,
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION,
save_runtime_overlay_import_document, save_runtime_save_slice_document,
save_runtime_snapshot_document,
};
use std::collections::BTreeMap;
#[test]
@ -268,7 +290,7 @@ mod tests {
original_save_sha256: None,
notes: vec![],
},
save_slice: rrt_runtime::SmpLoadedSaveSlice {
save_slice: SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
@ -335,7 +357,7 @@ mod tests {
}
#[test]
fn loads_fixture_from_relative_import_path() {
fn loads_fixture_from_relative_input_path() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
@ -362,9 +384,9 @@ mod tests {
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![rrt_runtime::RuntimeCompany {
companies: vec![RuntimeCompany {
company_id: 42,
controller_kind: rrt_runtime::RuntimeCompanyControllerKind::Human,
controller_kind: RuntimeCompanyControllerKind::Human,
current_cash: 100,
debt: 0,
credit_rating_score: None,
@ -417,7 +439,7 @@ mod tests {
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
save_slice_id: "slice".to_string(),
source: RuntimeSaveSliceDocumentSource::default(),
save_slice: rrt_runtime::SmpLoadedSaveSlice {
save_slice: SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
@ -440,8 +462,7 @@ mod tests {
placed_structure_collection: None,
placed_structure_dynamic_side_buffer_summary: None,
special_conditions_table: None,
event_runtime_collection: Some(
rrt_runtime::SmpLoadedEventRuntimeCollectionSummary {
event_runtime_collection: Some(SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
@ -470,7 +491,7 @@ mod tests {
add_building_dispatch_strip_condition_tuple_families: vec![],
add_building_dispatch_strip_signature_condition_clusters: vec![],
control_lane_notes: vec![],
records: vec![rrt_runtime::SmpLoadedPackedEventRecordSummary {
records: vec![SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 7,
payload_offset: Some(0x7202),
@ -489,15 +510,14 @@ mod tests {
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_conditions: Vec::new(),
decoded_actions: vec![rrt_runtime::RuntimeEffect::AdjustCompanyCash {
target: rrt_runtime::RuntimeCompanyTarget::Ids { ids: vec![42] },
decoded_actions: vec![RuntimeEffect::AdjustCompanyCash {
target: RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 25,
}],
executable_import_ready: false,
notes: vec![],
}],
},
),
}),
notes: vec![],
},
};
@ -521,7 +541,7 @@ mod tests {
"source": {
"kind": "captured-runtime"
},
"state_import_path": "overlay.json",
"state_input_path": "overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
@ -540,7 +560,7 @@ mod tests {
assert_eq!(
fixture.state_origin,
FixtureStateOrigin::ImportPath("overlay.json".to_string())
FixtureStateOrigin::InputPath("overlay.json".to_string())
);
assert_eq!(fixture.state.event_runtime_records.len(), 1);

View file

@ -1,6 +1,6 @@
use serde_json::Value;
use rrt_runtime::RuntimeState;
use rrt_runtime::state::RuntimeState;
pub fn normalize_runtime_state(state: &RuntimeState) -> Result<Value, Box<dyn std::error::Error>> {
Ok(serde_json::to_value(state)?)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,68 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use rrt_runtime::engine::StepCommand;
use rrt_runtime::state::RuntimeState;
use super::ExpectedRuntimeSummary;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct FixtureSource {
pub kind: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FixtureDocument {
pub format_version: u32,
pub fixture_id: String,
#[serde(default)]
pub source: FixtureSource,
pub state: RuntimeState,
pub state_origin: FixtureStateOrigin,
#[serde(default)]
pub commands: Vec<StepCommand>,
#[serde(default)]
pub expected_summary: ExpectedRuntimeSummary,
#[serde(default)]
pub expected_state_fragment: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FixtureStateOrigin {
Inline,
SnapshotPath(String),
SaveSlicePath(String),
InputPath(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RawFixtureDocument {
pub format_version: u32,
pub fixture_id: String,
#[serde(default)]
pub source: FixtureSource,
#[serde(default)]
pub state: Option<RuntimeState>,
#[serde(default)]
pub state_snapshot_path: Option<String>,
#[serde(default)]
pub state_save_slice_path: Option<String>,
#[serde(default)]
pub state_input_path: Option<String>,
#[serde(default)]
pub commands: Vec<StepCommand>,
#[serde(default)]
pub expected_summary: ExpectedRuntimeSummary,
#[serde(default)]
pub expected_state_fragment: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FixtureValidationReport {
pub fixture_id: String,
pub valid: bool,
pub issue_count: usize,
pub issues: Vec<String>,
}

View file

@ -0,0 +1,17 @@
pub mod document;
pub mod state_fragment;
pub mod summary;
mod summary_compare;
pub mod validate;
pub const FIXTURE_FORMAT_VERSION: u32 = 1;
pub use document::{
FixtureDocument, FixtureSource, FixtureStateOrigin, FixtureValidationReport, RawFixtureDocument,
};
pub use state_fragment::compare_expected_state_fragment;
pub use summary::ExpectedRuntimeSummary;
pub use validate::validate_fixture_document;
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,49 @@
use serde_json::Value;
pub fn compare_expected_state_fragment(expected: &Value, actual: &Value) -> Vec<String> {
let mut mismatches = Vec::new();
compare_expected_state_fragment_at_path("$", expected, actual, &mut mismatches);
mismatches
}
fn compare_expected_state_fragment_at_path(
path: &str,
expected: &Value,
actual: &Value,
mismatches: &mut Vec<String>,
) {
match (expected, actual) {
(Value::Object(expected_map), Value::Object(actual_map)) => {
for (key, expected_value) in expected_map {
let next_path = format!("{path}.{key}");
match actual_map.get(key) {
Some(actual_value) => compare_expected_state_fragment_at_path(
&next_path,
expected_value,
actual_value,
mismatches,
),
None => mismatches.push(format!("{next_path} missing in actual state")),
}
}
}
(Value::Array(expected_items), Value::Array(actual_items)) => {
for (index, expected_item) in expected_items.iter().enumerate() {
let next_path = format!("{path}[{index}]");
match actual_items.get(index) {
Some(actual_item) => compare_expected_state_fragment_at_path(
&next_path,
expected_item,
actual_item,
mismatches,
),
None => mismatches.push(format!("{next_path} missing in actual state")),
}
}
}
_ if expected != actual => mismatches.push(format!(
"{path} mismatch: expected {expected:?}, got {actual:?}"
)),
_ => {}
}
}

View file

@ -0,0 +1,302 @@
use serde::{Deserialize, Serialize};
use rrt_runtime::state::CalendarPoint;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub calendar: Option<CalendarPoint>,
#[serde(default)]
pub calendar_projection_source: Option<String>,
#[serde(default)]
pub calendar_projection_is_placeholder: Option<bool>,
#[serde(default)]
pub world_flag_count: Option<usize>,
#[serde(default)]
pub world_restore_selected_year_profile_lane: Option<u8>,
#[serde(default)]
pub world_restore_campaign_scenario_enabled: Option<bool>,
#[serde(default)]
pub world_restore_sandbox_enabled: Option<bool>,
#[serde(default)]
pub world_restore_seed_tuple_written_from_raw_lane: Option<bool>,
#[serde(default)]
pub world_restore_absolute_counter_requires_shell_context: Option<bool>,
#[serde(default)]
pub world_restore_absolute_counter_reconstructible_from_save: Option<bool>,
#[serde(default)]
pub world_restore_packed_year_word_raw_u16: Option<u16>,
#[serde(default)]
pub world_restore_partial_year_progress_raw_u8: Option<u8>,
#[serde(default)]
pub world_restore_current_calendar_tuple_word_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_current_calendar_tuple_word_2_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_absolute_counter_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_absolute_counter_mirror_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_slot: Option<u8>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_reconstructible_from_save:
Option<bool>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_write_side_grounded: Option<bool>,
#[serde(default)]
pub world_restore_disable_cargo_economy_special_condition_enabled: Option<bool>,
#[serde(default)]
pub world_restore_use_bio_accelerator_cars_enabled: Option<bool>,
#[serde(default)]
pub world_restore_use_wartime_cargos_enabled: Option<bool>,
#[serde(default)]
pub world_restore_disable_train_crashes_enabled: Option<bool>,
#[serde(default)]
pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option<bool>,
#[serde(default)]
pub world_restore_ai_ignore_territories_at_startup_enabled: Option<bool>,
#[serde(default)]
pub world_restore_limited_track_building_amount: Option<i32>,
#[serde(default)]
pub world_restore_economic_status_code: Option<i32>,
#[serde(default)]
pub world_restore_territory_access_cost: Option<u32>,
#[serde(default)]
pub world_restore_issue_37_value: Option<u32>,
#[serde(default)]
pub world_restore_issue_38_value: Option<u32>,
#[serde(default)]
pub world_restore_issue_39_value: Option<u32>,
#[serde(default)]
pub world_restore_issue_3a_value: Option<u32>,
#[serde(default)]
pub world_restore_issue_37_multiplier_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_issue_37_multiplier_value_f32_text: Option<String>,
#[serde(default)]
pub world_restore_finance_neighborhood_count: Option<usize>,
#[serde(default)]
pub world_restore_finance_neighborhood_labels: Option<Vec<String>>,
#[serde(default)]
pub world_restore_economic_tuning_mirror_raw_u32: Option<u32>,
#[serde(default)]
pub world_restore_economic_tuning_mirror_value_f32_text: Option<String>,
#[serde(default)]
pub world_restore_economic_tuning_lane_count: Option<usize>,
#[serde(default)]
pub world_restore_economic_tuning_lane_value_f32_text: Option<Vec<String>>,
#[serde(default)]
pub world_restore_absolute_counter_restore_kind: Option<String>,
#[serde(default)]
pub world_restore_absolute_counter_adjustment_context: Option<String>,
#[serde(default)]
pub metadata_count: Option<usize>,
#[serde(default)]
pub company_count: Option<usize>,
#[serde(default)]
pub active_company_count: Option<usize>,
#[serde(default)]
pub company_market_state_owner_count: Option<usize>,
#[serde(default)]
pub selected_company_outstanding_shares: Option<u32>,
#[serde(default)]
pub selected_company_bond_count: Option<u8>,
#[serde(default)]
pub selected_company_largest_live_bond_principal: Option<u32>,
#[serde(default)]
pub selected_company_highest_coupon_live_bond_principal: Option<u32>,
#[serde(default)]
pub selected_company_assigned_share_pool: Option<u32>,
#[serde(default)]
pub selected_company_unassigned_share_pool: Option<u32>,
#[serde(default)]
pub selected_company_cached_share_price: Option<i64>,
#[serde(default)]
pub selected_company_cached_share_price_value_f32_text: Option<String>,
#[serde(default)]
pub selected_company_mutable_support_scalar_value_f32_text: Option<String>,
#[serde(default)]
pub selected_company_stat_band_root_0cfb_count: Option<usize>,
#[serde(default)]
pub selected_company_stat_band_root_0d7f_count: Option<usize>,
#[serde(default)]
pub selected_company_stat_band_root_1c47_count: Option<usize>,
#[serde(default)]
pub selected_company_last_dividend_year: Option<u32>,
#[serde(default)]
pub selected_company_years_since_founding: Option<u32>,
#[serde(default)]
pub selected_company_years_since_last_bankruptcy: Option<u32>,
#[serde(default)]
pub selected_company_years_since_last_dividend: Option<u32>,
#[serde(default)]
pub selected_company_current_partial_year_weight_numerator: Option<i64>,
#[serde(default)]
pub selected_company_current_issue_absolute_counter: Option<u32>,
#[serde(default)]
pub selected_company_prior_issue_absolute_counter: Option<u32>,
#[serde(default)]
pub selected_company_current_issue_age_absolute_counter_delta: Option<i64>,
#[serde(default)]
pub selected_company_chairman_bonus_year: Option<u32>,
#[serde(default)]
pub selected_company_chairman_bonus_amount: Option<i32>,
#[serde(default)]
pub player_count: Option<usize>,
#[serde(default)]
pub chairman_profile_count: Option<usize>,
#[serde(default)]
pub active_chairman_profile_count: Option<usize>,
#[serde(default)]
pub selected_chairman_profile_id: Option<u32>,
#[serde(default)]
pub linked_chairman_company_count: Option<usize>,
#[serde(default)]
pub company_takeover_cooldown_count: Option<usize>,
#[serde(default)]
pub company_merger_cooldown_count: Option<usize>,
#[serde(default)]
pub train_count: Option<usize>,
#[serde(default)]
pub active_train_count: Option<usize>,
#[serde(default)]
pub retired_train_count: Option<usize>,
#[serde(default)]
pub locomotive_catalog_count: Option<usize>,
#[serde(default)]
pub cargo_catalog_count: Option<usize>,
#[serde(default)]
pub territory_count: Option<usize>,
#[serde(default)]
pub company_territory_track_count: Option<usize>,
#[serde(default)]
pub packed_event_collection_present: Option<bool>,
#[serde(default)]
pub packed_event_record_count: Option<usize>,
#[serde(default)]
pub packed_event_decoded_record_count: Option<usize>,
#[serde(default)]
pub packed_event_imported_runtime_record_count: Option<usize>,
#[serde(default)]
pub packed_event_parity_only_record_count: Option<usize>,
#[serde(default)]
pub packed_event_unsupported_record_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_company_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_selection_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_company_role_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_player_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_player_selection_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_player_role_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_chairman_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_chairman_target_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_condition_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_player_condition_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_company_condition_scope_disabled_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_player_condition_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_territory_condition_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_territory_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_named_territory_binding_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_unmapped_ordinary_condition_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_unmapped_world_condition_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_compact_control_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_shell_owned_descriptor_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_evidence_blocked_descriptor_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_variant_or_scope_blocked_descriptor_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_unmapped_real_descriptor_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_unmapped_world_descriptor_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_territory_access_variant_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_territory_access_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_train_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_train_territory_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_missing_locomotive_catalog_context_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_confiscation_variant_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_retire_train_variant_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_retire_train_scope_count: Option<usize>,
#[serde(default)]
pub packed_event_blocked_structural_only_count: Option<usize>,
#[serde(default)]
pub event_runtime_record_count: Option<usize>,
#[serde(default)]
pub candidate_availability_count: Option<usize>,
#[serde(default)]
pub zero_candidate_availability_count: Option<usize>,
#[serde(default)]
pub named_locomotive_availability_count: Option<usize>,
#[serde(default)]
pub zero_named_locomotive_availability_count: Option<usize>,
#[serde(default)]
pub named_locomotive_cost_count: Option<usize>,
#[serde(default)]
pub cargo_production_override_count: Option<usize>,
#[serde(default)]
pub world_runtime_variable_count: Option<usize>,
#[serde(default)]
pub company_runtime_variable_owner_count: Option<usize>,
#[serde(default)]
pub player_runtime_variable_owner_count: Option<usize>,
#[serde(default)]
pub territory_runtime_variable_owner_count: Option<usize>,
#[serde(default)]
pub world_scalar_override_count: Option<usize>,
#[serde(default)]
pub special_condition_count: Option<usize>,
#[serde(default)]
pub enabled_special_condition_count: Option<usize>,
#[serde(default)]
pub save_profile_kind: Option<String>,
#[serde(default)]
pub save_profile_family: Option<String>,
#[serde(default)]
pub save_profile_map_path: Option<String>,
#[serde(default)]
pub save_profile_display_name: Option<String>,
#[serde(default)]
pub save_profile_selected_year_profile_lane: Option<u8>,
#[serde(default)]
pub save_profile_sandbox_enabled: Option<bool>,
#[serde(default)]
pub save_profile_campaign_scenario_enabled: Option<bool>,
#[serde(default)]
pub save_profile_staged_profile_copy_on_restore: Option<bool>,
#[serde(default)]
pub total_event_record_service_count: Option<u64>,
#[serde(default)]
pub periodic_boundary_call_count: Option<u64>,
#[serde(default)]
pub total_trigger_dispatch_count: Option<u64>,
#[serde(default)]
pub dirty_rerun_count: Option<u64>,
#[serde(default)]
pub total_company_cash: Option<i64>,
}

View file

@ -0,0 +1,345 @@
use rrt_runtime::summary::RuntimeSummary;
use crate::schema::ExpectedRuntimeSummary;
pub(super) fn compare_collections_prefix(
expected: &ExpectedRuntimeSummary,
actual: &RuntimeSummary,
mismatches: &mut Vec<String>,
) {
if let Some(count) = expected.player_count {
if actual.player_count != count {
mismatches.push(format!(
"player_count mismatch: expected {count}, got {}",
actual.player_count
));
}
}
if let Some(count) = expected.chairman_profile_count {
if actual.chairman_profile_count != count {
mismatches.push(format!(
"chairman_profile_count mismatch: expected {count}, got {}",
actual.chairman_profile_count
));
}
}
if let Some(count) = expected.active_chairman_profile_count {
if actual.active_chairman_profile_count != count {
mismatches.push(format!(
"active_chairman_profile_count mismatch: expected {count}, got {}",
actual.active_chairman_profile_count
));
}
}
if let Some(selected_id) = expected.selected_chairman_profile_id {
if actual.selected_chairman_profile_id != Some(selected_id) {
mismatches.push(format!(
"selected_chairman_profile_id mismatch: expected {selected_id:?}, got {:?}",
actual.selected_chairman_profile_id
));
}
}
if let Some(count) = expected.linked_chairman_company_count {
if actual.linked_chairman_company_count != count {
mismatches.push(format!(
"linked_chairman_company_count mismatch: expected {count}, got {}",
actual.linked_chairman_company_count
));
}
}
if let Some(count) = expected.company_takeover_cooldown_count {
if actual.company_takeover_cooldown_count != count {
mismatches.push(format!(
"company_takeover_cooldown_count mismatch: expected {count}, got {}",
actual.company_takeover_cooldown_count
));
}
}
if let Some(count) = expected.company_merger_cooldown_count {
if actual.company_merger_cooldown_count != count {
mismatches.push(format!(
"company_merger_cooldown_count mismatch: expected {count}, got {}",
actual.company_merger_cooldown_count
));
}
}
if let Some(count) = expected.train_count {
if actual.train_count != count {
mismatches.push(format!(
"train_count mismatch: expected {count}, got {}",
actual.train_count
));
}
}
if let Some(count) = expected.active_train_count {
if actual.active_train_count != count {
mismatches.push(format!(
"active_train_count mismatch: expected {count}, got {}",
actual.active_train_count
));
}
}
if let Some(count) = expected.retired_train_count {
if actual.retired_train_count != count {
mismatches.push(format!(
"retired_train_count mismatch: expected {count}, got {}",
actual.retired_train_count
));
}
}
if let Some(count) = expected.locomotive_catalog_count {
if actual.locomotive_catalog_count != count {
mismatches.push(format!(
"locomotive_catalog_count mismatch: expected {count}, got {}",
actual.locomotive_catalog_count
));
}
}
if let Some(count) = expected.cargo_catalog_count {
if actual.cargo_catalog_count != count {
mismatches.push(format!(
"cargo_catalog_count mismatch: expected {count}, got {}",
actual.cargo_catalog_count
));
}
}
if let Some(count) = expected.territory_count {
if actual.territory_count != count {
mismatches.push(format!(
"territory_count mismatch: expected {count}, got {}",
actual.territory_count
));
}
}
if let Some(count) = expected.company_territory_track_count {
if actual.company_territory_track_count != count {
mismatches.push(format!(
"company_territory_track_count mismatch: expected {count}, got {}",
actual.company_territory_track_count
));
}
}
}
pub(super) fn compare_collections_suffix(
expected: &ExpectedRuntimeSummary,
actual: &RuntimeSummary,
mismatches: &mut Vec<String>,
) {
if let Some(count) = expected.event_runtime_record_count {
if actual.event_runtime_record_count != count {
mismatches.push(format!(
"event_runtime_record_count mismatch: expected {count}, got {}",
actual.event_runtime_record_count
));
}
}
if let Some(count) = expected.candidate_availability_count {
if actual.candidate_availability_count != count {
mismatches.push(format!(
"candidate_availability_count mismatch: expected {count}, got {}",
actual.candidate_availability_count
));
}
}
if let Some(count) = expected.zero_candidate_availability_count {
if actual.zero_candidate_availability_count != count {
mismatches.push(format!(
"zero_candidate_availability_count mismatch: expected {count}, got {}",
actual.zero_candidate_availability_count
));
}
}
if let Some(count) = expected.named_locomotive_availability_count {
if actual.named_locomotive_availability_count != count {
mismatches.push(format!(
"named_locomotive_availability_count mismatch: expected {count}, got {}",
actual.named_locomotive_availability_count
));
}
}
if let Some(count) = expected.zero_named_locomotive_availability_count {
if actual.zero_named_locomotive_availability_count != count {
mismatches.push(format!(
"zero_named_locomotive_availability_count mismatch: expected {count}, got {}",
actual.zero_named_locomotive_availability_count
));
}
}
if let Some(count) = expected.named_locomotive_cost_count {
if actual.named_locomotive_cost_count != count {
mismatches.push(format!(
"named_locomotive_cost_count mismatch: expected {count}, got {}",
actual.named_locomotive_cost_count
));
}
}
if let Some(count) = expected.cargo_production_override_count {
if actual.cargo_production_override_count != count {
mismatches.push(format!(
"cargo_production_override_count mismatch: expected {count}, got {}",
actual.cargo_production_override_count
));
}
}
if let Some(count) = expected.world_runtime_variable_count {
if actual.world_runtime_variable_count != count {
mismatches.push(format!(
"world_runtime_variable_count mismatch: expected {count}, got {}",
actual.world_runtime_variable_count
));
}
}
if let Some(count) = expected.company_runtime_variable_owner_count {
if actual.company_runtime_variable_owner_count != count {
mismatches.push(format!(
"company_runtime_variable_owner_count mismatch: expected {count}, got {}",
actual.company_runtime_variable_owner_count
));
}
}
if let Some(count) = expected.player_runtime_variable_owner_count {
if actual.player_runtime_variable_owner_count != count {
mismatches.push(format!(
"player_runtime_variable_owner_count mismatch: expected {count}, got {}",
actual.player_runtime_variable_owner_count
));
}
}
if let Some(count) = expected.territory_runtime_variable_owner_count {
if actual.territory_runtime_variable_owner_count != count {
mismatches.push(format!(
"territory_runtime_variable_owner_count mismatch: expected {count}, got {}",
actual.territory_runtime_variable_owner_count
));
}
}
if let Some(count) = expected.world_scalar_override_count {
if actual.world_scalar_override_count != count {
mismatches.push(format!(
"world_scalar_override_count mismatch: expected {count}, got {}",
actual.world_scalar_override_count
));
}
}
if let Some(count) = expected.special_condition_count {
if actual.special_condition_count != count {
mismatches.push(format!(
"special_condition_count mismatch: expected {count}, got {}",
actual.special_condition_count
));
}
}
if let Some(count) = expected.enabled_special_condition_count {
if actual.enabled_special_condition_count != count {
mismatches.push(format!(
"enabled_special_condition_count mismatch: expected {count}, got {}",
actual.enabled_special_condition_count
));
}
}
if let Some(kind) = &expected.save_profile_kind {
if actual.save_profile_kind.as_ref() != Some(kind) {
mismatches.push(format!(
"save_profile_kind mismatch: expected {kind:?}, got {:?}",
actual.save_profile_kind
));
}
}
if let Some(family) = &expected.save_profile_family {
if actual.save_profile_family.as_ref() != Some(family) {
mismatches.push(format!(
"save_profile_family mismatch: expected {family:?}, got {:?}",
actual.save_profile_family
));
}
}
if let Some(map_path) = &expected.save_profile_map_path {
if actual.save_profile_map_path.as_ref() != Some(map_path) {
mismatches.push(format!(
"save_profile_map_path mismatch: expected {map_path:?}, got {:?}",
actual.save_profile_map_path
));
}
}
if let Some(display_name) = &expected.save_profile_display_name {
if actual.save_profile_display_name.as_ref() != Some(display_name) {
mismatches.push(format!(
"save_profile_display_name mismatch: expected {display_name:?}, got {:?}",
actual.save_profile_display_name
));
}
}
if let Some(lane) = expected.save_profile_selected_year_profile_lane {
if actual.save_profile_selected_year_profile_lane != Some(lane) {
mismatches.push(format!(
"save_profile_selected_year_profile_lane mismatch: expected {lane}, got {:?}",
actual.save_profile_selected_year_profile_lane
));
}
}
if let Some(enabled) = expected.save_profile_sandbox_enabled {
if actual.save_profile_sandbox_enabled != Some(enabled) {
mismatches.push(format!(
"save_profile_sandbox_enabled mismatch: expected {enabled}, got {:?}",
actual.save_profile_sandbox_enabled
));
}
}
if let Some(enabled) = expected.save_profile_campaign_scenario_enabled {
if actual.save_profile_campaign_scenario_enabled != Some(enabled) {
mismatches.push(format!(
"save_profile_campaign_scenario_enabled mismatch: expected {enabled}, got {:?}",
actual.save_profile_campaign_scenario_enabled
));
}
}
if let Some(enabled) = expected.save_profile_staged_profile_copy_on_restore {
if actual.save_profile_staged_profile_copy_on_restore != Some(enabled) {
mismatches.push(format!(
"save_profile_staged_profile_copy_on_restore mismatch: expected {enabled}, got {:?}",
actual.save_profile_staged_profile_copy_on_restore
));
}
}
if let Some(count) = expected.total_event_record_service_count {
if actual.total_event_record_service_count != count {
mismatches.push(format!(
"total_event_record_service_count mismatch: expected {count}, got {}",
actual.total_event_record_service_count
));
}
}
if let Some(count) = expected.periodic_boundary_call_count {
if actual.periodic_boundary_call_count != count {
mismatches.push(format!(
"periodic_boundary_call_count mismatch: expected {count}, got {}",
actual.periodic_boundary_call_count
));
}
}
if let Some(count) = expected.total_trigger_dispatch_count {
if actual.total_trigger_dispatch_count != count {
mismatches.push(format!(
"total_trigger_dispatch_count mismatch: expected {count}, got {}",
actual.total_trigger_dispatch_count
));
}
}
if let Some(count) = expected.dirty_rerun_count {
if actual.dirty_rerun_count != count {
mismatches.push(format!(
"dirty_rerun_count mismatch: expected {count}, got {}",
actual.dirty_rerun_count
));
}
}
if let Some(total) = expected.total_company_cash {
if actual.total_company_cash != total {
mismatches.push(format!(
"total_company_cash mismatch: expected {total}, got {}",
actual.total_company_cash
));
}
}
}

View file

@ -0,0 +1,22 @@
use rrt_runtime::summary::RuntimeSummary;
use super::ExpectedRuntimeSummary;
mod collections;
mod packed_events;
mod selected_company;
mod world;
impl ExpectedRuntimeSummary {
pub fn compare(&self, actual: &RuntimeSummary) -> Vec<String> {
let mut mismatches = Vec::new();
world::compare_world(self, actual, &mut mismatches);
selected_company::compare_selected_company(self, actual, &mut mismatches);
collections::compare_collections_prefix(self, actual, &mut mismatches);
packed_events::compare_packed_events(self, actual, &mut mismatches);
collections::compare_collections_suffix(self, actual, &mut mismatches);
mismatches
}
}

View file

@ -0,0 +1,314 @@
use rrt_runtime::summary::RuntimeSummary;
use crate::schema::ExpectedRuntimeSummary;
pub(super) fn compare_packed_events(
expected: &ExpectedRuntimeSummary,
actual: &RuntimeSummary,
mismatches: &mut Vec<String>,
) {
if let Some(present) = expected.packed_event_collection_present {
if actual.packed_event_collection_present != present {
mismatches.push(format!(
"packed_event_collection_present mismatch: expected {present}, got {}",
actual.packed_event_collection_present
));
}
}
if let Some(count) = expected.packed_event_record_count {
if actual.packed_event_record_count != count {
mismatches.push(format!(
"packed_event_record_count mismatch: expected {count}, got {}",
actual.packed_event_record_count
));
}
}
if let Some(count) = expected.packed_event_decoded_record_count {
if actual.packed_event_decoded_record_count != count {
mismatches.push(format!(
"packed_event_decoded_record_count mismatch: expected {count}, got {}",
actual.packed_event_decoded_record_count
));
}
}
if let Some(count) = expected.packed_event_imported_runtime_record_count {
if actual.packed_event_imported_runtime_record_count != count {
mismatches.push(format!(
"packed_event_imported_runtime_record_count mismatch: expected {count}, got {}",
actual.packed_event_imported_runtime_record_count
));
}
}
if let Some(count) = expected.packed_event_parity_only_record_count {
if actual.packed_event_parity_only_record_count != count {
mismatches.push(format!(
"packed_event_parity_only_record_count mismatch: expected {count}, got {}",
actual.packed_event_parity_only_record_count
));
}
}
if let Some(count) = expected.packed_event_unsupported_record_count {
if actual.packed_event_unsupported_record_count != count {
mismatches.push(format!(
"packed_event_unsupported_record_count mismatch: expected {count}, got {}",
actual.packed_event_unsupported_record_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_company_context_count {
if actual.packed_event_blocked_missing_company_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_company_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_company_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_selection_context_count {
if actual.packed_event_blocked_missing_selection_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_selection_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_selection_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_company_role_context_count {
if actual.packed_event_blocked_missing_company_role_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_company_role_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_company_role_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_player_context_count {
if actual.packed_event_blocked_missing_player_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_player_selection_context_count {
if actual.packed_event_blocked_missing_player_selection_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_selection_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_selection_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_player_role_context_count {
if actual.packed_event_blocked_missing_player_role_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_role_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_role_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_chairman_context_count {
if actual.packed_event_blocked_missing_chairman_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_chairman_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_chairman_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_chairman_target_scope_count {
if actual.packed_event_blocked_chairman_target_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_chairman_target_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_chairman_target_scope_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_condition_context_count {
if actual.packed_event_blocked_missing_condition_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_condition_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_condition_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_player_condition_context_count {
if actual.packed_event_blocked_missing_player_condition_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_player_condition_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_player_condition_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_company_condition_scope_disabled_count {
if actual.packed_event_blocked_company_condition_scope_disabled_count != count {
mismatches.push(format!(
"packed_event_blocked_company_condition_scope_disabled_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_company_condition_scope_disabled_count
));
}
}
if let Some(count) = expected.packed_event_blocked_player_condition_scope_count {
if actual.packed_event_blocked_player_condition_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_player_condition_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_player_condition_scope_count
));
}
}
if let Some(count) = expected.packed_event_blocked_territory_condition_scope_count {
if actual.packed_event_blocked_territory_condition_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_territory_condition_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_territory_condition_scope_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_territory_context_count {
if actual.packed_event_blocked_missing_territory_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_territory_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_territory_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_named_territory_binding_count {
if actual.packed_event_blocked_named_territory_binding_count != count {
mismatches.push(format!(
"packed_event_blocked_named_territory_binding_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_named_territory_binding_count
));
}
}
if let Some(count) = expected.packed_event_blocked_unmapped_ordinary_condition_count {
if actual.packed_event_blocked_unmapped_ordinary_condition_count != count {
mismatches.push(format!(
"packed_event_blocked_unmapped_ordinary_condition_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_unmapped_ordinary_condition_count
));
}
}
if let Some(count) = expected.packed_event_blocked_unmapped_world_condition_count {
if actual.packed_event_blocked_unmapped_world_condition_count != count {
mismatches.push(format!(
"packed_event_blocked_unmapped_world_condition_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_unmapped_world_condition_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_compact_control_count {
if actual.packed_event_blocked_missing_compact_control_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_compact_control_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_compact_control_count
));
}
}
if let Some(count) = expected.packed_event_blocked_shell_owned_descriptor_count {
if actual.packed_event_blocked_shell_owned_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_shell_owned_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_shell_owned_descriptor_count
));
}
}
if let Some(count) = expected.packed_event_blocked_evidence_blocked_descriptor_count {
if actual.packed_event_blocked_evidence_blocked_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_evidence_blocked_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_evidence_blocked_descriptor_count
));
}
}
if let Some(count) = expected.packed_event_blocked_variant_or_scope_blocked_descriptor_count {
if actual.packed_event_blocked_variant_or_scope_blocked_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_variant_or_scope_blocked_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_variant_or_scope_blocked_descriptor_count
));
}
}
if let Some(count) = expected.packed_event_blocked_unmapped_real_descriptor_count {
if actual.packed_event_blocked_unmapped_real_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_unmapped_real_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_unmapped_real_descriptor_count
));
}
}
if let Some(count) = expected.packed_event_blocked_unmapped_world_descriptor_count {
if actual.packed_event_blocked_unmapped_world_descriptor_count != count {
mismatches.push(format!(
"packed_event_blocked_unmapped_world_descriptor_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_unmapped_world_descriptor_count
));
}
}
if let Some(count) = expected.packed_event_blocked_territory_access_variant_count {
if actual.packed_event_blocked_territory_access_variant_count != count {
mismatches.push(format!(
"packed_event_blocked_territory_access_variant_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_territory_access_variant_count
));
}
}
if let Some(count) = expected.packed_event_blocked_territory_access_scope_count {
if actual.packed_event_blocked_territory_access_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_territory_access_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_territory_access_scope_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_train_context_count {
if actual.packed_event_blocked_missing_train_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_train_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_train_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_train_territory_context_count {
if actual.packed_event_blocked_missing_train_territory_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_train_territory_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_train_territory_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_missing_locomotive_catalog_context_count {
if actual.packed_event_blocked_missing_locomotive_catalog_context_count != count {
mismatches.push(format!(
"packed_event_blocked_missing_locomotive_catalog_context_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_missing_locomotive_catalog_context_count
));
}
}
if let Some(count) = expected.packed_event_blocked_confiscation_variant_count {
if actual.packed_event_blocked_confiscation_variant_count != count {
mismatches.push(format!(
"packed_event_blocked_confiscation_variant_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_confiscation_variant_count
));
}
}
if let Some(count) = expected.packed_event_blocked_retire_train_variant_count {
if actual.packed_event_blocked_retire_train_variant_count != count {
mismatches.push(format!(
"packed_event_blocked_retire_train_variant_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_retire_train_variant_count
));
}
}
if let Some(count) = expected.packed_event_blocked_retire_train_scope_count {
if actual.packed_event_blocked_retire_train_scope_count != count {
mismatches.push(format!(
"packed_event_blocked_retire_train_scope_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_retire_train_scope_count
));
}
}
if let Some(count) = expected.packed_event_blocked_structural_only_count {
if actual.packed_event_blocked_structural_only_count != count {
mismatches.push(format!(
"packed_event_blocked_structural_only_count mismatch: expected {count}, got {}",
actual.packed_event_blocked_structural_only_count
));
}
}
}

View file

@ -0,0 +1,194 @@
use rrt_runtime::summary::RuntimeSummary;
use crate::schema::ExpectedRuntimeSummary;
pub(super) fn compare_selected_company(
expected: &ExpectedRuntimeSummary,
actual: &RuntimeSummary,
mismatches: &mut Vec<String>,
) {
if let Some(value) = expected.selected_company_outstanding_shares {
if actual.selected_company_outstanding_shares != Some(value) {
mismatches.push(format!(
"selected_company_outstanding_shares mismatch: expected {value}, got {:?}",
actual.selected_company_outstanding_shares
));
}
}
if let Some(value) = expected.selected_company_bond_count {
if actual.selected_company_bond_count != Some(value) {
mismatches.push(format!(
"selected_company_bond_count mismatch: expected {value}, got {:?}",
actual.selected_company_bond_count
));
}
}
if let Some(value) = expected.selected_company_largest_live_bond_principal {
if actual.selected_company_largest_live_bond_principal != Some(value) {
mismatches.push(format!(
"selected_company_largest_live_bond_principal mismatch: expected {value}, got {:?}",
actual.selected_company_largest_live_bond_principal
));
}
}
if let Some(value) = expected.selected_company_highest_coupon_live_bond_principal {
if actual.selected_company_highest_coupon_live_bond_principal != Some(value) {
mismatches.push(format!(
"selected_company_highest_coupon_live_bond_principal mismatch: expected {value}, got {:?}",
actual.selected_company_highest_coupon_live_bond_principal
));
}
}
if let Some(value) = expected.selected_company_assigned_share_pool {
if actual.selected_company_assigned_share_pool != Some(value) {
mismatches.push(format!(
"selected_company_assigned_share_pool mismatch: expected {value}, got {:?}",
actual.selected_company_assigned_share_pool
));
}
}
if let Some(value) = expected.selected_company_unassigned_share_pool {
if actual.selected_company_unassigned_share_pool != Some(value) {
mismatches.push(format!(
"selected_company_unassigned_share_pool mismatch: expected {value}, got {:?}",
actual.selected_company_unassigned_share_pool
));
}
}
if let Some(value) = expected.selected_company_cached_share_price {
if actual.selected_company_cached_share_price != Some(value) {
mismatches.push(format!(
"selected_company_cached_share_price mismatch: expected {value}, got {:?}",
actual.selected_company_cached_share_price
));
}
}
if let Some(value) = &expected.selected_company_cached_share_price_value_f32_text {
if actual
.selected_company_cached_share_price_value_f32_text
.as_ref()
!= Some(value)
{
mismatches.push(format!(
"selected_company_cached_share_price_value_f32_text mismatch: expected {value:?}, got {:?}",
actual.selected_company_cached_share_price_value_f32_text
));
}
}
if let Some(value) = &expected.selected_company_mutable_support_scalar_value_f32_text {
if actual
.selected_company_mutable_support_scalar_value_f32_text
.as_ref()
!= Some(value)
{
mismatches.push(format!(
"selected_company_mutable_support_scalar_value_f32_text mismatch: expected {value:?}, got {:?}",
actual.selected_company_mutable_support_scalar_value_f32_text
));
}
}
if let Some(count) = expected.selected_company_stat_band_root_0cfb_count {
if actual.selected_company_stat_band_root_0cfb_count != count {
mismatches.push(format!(
"selected_company_stat_band_root_0cfb_count mismatch: expected {count}, got {}",
actual.selected_company_stat_band_root_0cfb_count
));
}
}
if let Some(count) = expected.selected_company_stat_band_root_0d7f_count {
if actual.selected_company_stat_band_root_0d7f_count != count {
mismatches.push(format!(
"selected_company_stat_band_root_0d7f_count mismatch: expected {count}, got {}",
actual.selected_company_stat_band_root_0d7f_count
));
}
}
if let Some(count) = expected.selected_company_stat_band_root_1c47_count {
if actual.selected_company_stat_band_root_1c47_count != count {
mismatches.push(format!(
"selected_company_stat_band_root_1c47_count mismatch: expected {count}, got {}",
actual.selected_company_stat_band_root_1c47_count
));
}
}
if let Some(value) = expected.selected_company_last_dividend_year {
if actual.selected_company_last_dividend_year != Some(value) {
mismatches.push(format!(
"selected_company_last_dividend_year mismatch: expected {value}, got {:?}",
actual.selected_company_last_dividend_year
));
}
}
if let Some(value) = expected.selected_company_years_since_founding {
if actual.selected_company_years_since_founding != Some(value) {
mismatches.push(format!(
"selected_company_years_since_founding mismatch: expected {value}, got {:?}",
actual.selected_company_years_since_founding
));
}
}
if let Some(value) = expected.selected_company_years_since_last_bankruptcy {
if actual.selected_company_years_since_last_bankruptcy != Some(value) {
mismatches.push(format!(
"selected_company_years_since_last_bankruptcy mismatch: expected {value}, got {:?}",
actual.selected_company_years_since_last_bankruptcy
));
}
}
if let Some(value) = expected.selected_company_years_since_last_dividend {
if actual.selected_company_years_since_last_dividend != Some(value) {
mismatches.push(format!(
"selected_company_years_since_last_dividend mismatch: expected {value}, got {:?}",
actual.selected_company_years_since_last_dividend
));
}
}
if let Some(value) = expected.selected_company_current_partial_year_weight_numerator {
if actual.selected_company_current_partial_year_weight_numerator != Some(value) {
mismatches.push(format!(
"selected_company_current_partial_year_weight_numerator mismatch: expected {value}, got {:?}",
actual.selected_company_current_partial_year_weight_numerator
));
}
}
if let Some(value) = expected.selected_company_current_issue_absolute_counter {
if actual.selected_company_current_issue_absolute_counter != Some(value) {
mismatches.push(format!(
"selected_company_current_issue_absolute_counter mismatch: expected {value}, got {:?}",
actual.selected_company_current_issue_absolute_counter
));
}
}
if let Some(value) = expected.selected_company_prior_issue_absolute_counter {
if actual.selected_company_prior_issue_absolute_counter != Some(value) {
mismatches.push(format!(
"selected_company_prior_issue_absolute_counter mismatch: expected {value}, got {:?}",
actual.selected_company_prior_issue_absolute_counter
));
}
}
if let Some(value) = expected.selected_company_current_issue_age_absolute_counter_delta {
if actual.selected_company_current_issue_age_absolute_counter_delta != Some(value) {
mismatches.push(format!(
"selected_company_current_issue_age_absolute_counter_delta mismatch: expected {value}, got {:?}",
actual.selected_company_current_issue_age_absolute_counter_delta
));
}
}
if let Some(value) = expected.selected_company_chairman_bonus_year {
if actual.selected_company_chairman_bonus_year != Some(value) {
mismatches.push(format!(
"selected_company_chairman_bonus_year mismatch: expected {value}, got {:?}",
actual.selected_company_chairman_bonus_year
));
}
}
if let Some(value) = expected.selected_company_chairman_bonus_amount {
if actual.selected_company_chairman_bonus_amount != Some(value) {
mismatches.push(format!(
"selected_company_chairman_bonus_amount mismatch: expected {value}, got {:?}",
actual.selected_company_chairman_bonus_amount
));
}
}
}

View file

@ -0,0 +1,398 @@
use rrt_runtime::summary::RuntimeSummary;
use crate::schema::ExpectedRuntimeSummary;
pub(super) fn compare_world(
expected: &ExpectedRuntimeSummary,
actual: &RuntimeSummary,
mismatches: &mut Vec<String>,
) {
if let Some(calendar) = expected.calendar {
if actual.calendar != calendar {
mismatches.push(format!(
"calendar mismatch: expected {:?}, got {:?}",
calendar, actual.calendar
));
}
}
if let Some(source) = &expected.calendar_projection_source {
if actual.calendar_projection_source.as_ref() != Some(source) {
mismatches.push(format!(
"calendar_projection_source mismatch: expected {source:?}, got {:?}",
actual.calendar_projection_source
));
}
}
if let Some(is_placeholder) = expected.calendar_projection_is_placeholder {
if actual.calendar_projection_is_placeholder != is_placeholder {
mismatches.push(format!(
"calendar_projection_is_placeholder mismatch: expected {is_placeholder}, got {}",
actual.calendar_projection_is_placeholder
));
}
}
if let Some(count) = expected.world_flag_count {
if actual.world_flag_count != count {
mismatches.push(format!(
"world_flag_count mismatch: expected {count}, got {}",
actual.world_flag_count
));
}
}
if let Some(lane) = expected.world_restore_selected_year_profile_lane {
if actual.world_restore_selected_year_profile_lane != Some(lane) {
mismatches.push(format!(
"world_restore_selected_year_profile_lane mismatch: expected {lane}, got {:?}",
actual.world_restore_selected_year_profile_lane
));
}
}
if let Some(enabled) = expected.world_restore_campaign_scenario_enabled {
if actual.world_restore_campaign_scenario_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_campaign_scenario_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_campaign_scenario_enabled
));
}
}
if let Some(enabled) = expected.world_restore_sandbox_enabled {
if actual.world_restore_sandbox_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_sandbox_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_sandbox_enabled
));
}
}
if let Some(enabled) = expected.world_restore_seed_tuple_written_from_raw_lane {
if actual.world_restore_seed_tuple_written_from_raw_lane != Some(enabled) {
mismatches.push(format!(
"world_restore_seed_tuple_written_from_raw_lane mismatch: expected {enabled}, got {:?}",
actual.world_restore_seed_tuple_written_from_raw_lane
));
}
}
if let Some(enabled) = expected.world_restore_absolute_counter_requires_shell_context {
if actual.world_restore_absolute_counter_requires_shell_context != Some(enabled) {
mismatches.push(format!(
"world_restore_absolute_counter_requires_shell_context mismatch: expected {enabled}, got {:?}",
actual.world_restore_absolute_counter_requires_shell_context
));
}
}
if let Some(enabled) = expected.world_restore_absolute_counter_reconstructible_from_save {
if actual.world_restore_absolute_counter_reconstructible_from_save != Some(enabled) {
mismatches.push(format!(
"world_restore_absolute_counter_reconstructible_from_save mismatch: expected {enabled}, got {:?}",
actual.world_restore_absolute_counter_reconstructible_from_save
));
}
}
if let Some(value) = expected.world_restore_packed_year_word_raw_u16 {
if actual.world_restore_packed_year_word_raw_u16 != Some(value) {
mismatches.push(format!(
"world_restore_packed_year_word_raw_u16 mismatch: expected {value}, got {:?}",
actual.world_restore_packed_year_word_raw_u16
));
}
}
if let Some(value) = expected.world_restore_partial_year_progress_raw_u8 {
if actual.world_restore_partial_year_progress_raw_u8 != Some(value) {
mismatches.push(format!(
"world_restore_partial_year_progress_raw_u8 mismatch: expected {value}, got {:?}",
actual.world_restore_partial_year_progress_raw_u8
));
}
}
if let Some(value) = expected.world_restore_current_calendar_tuple_word_raw_u32 {
if actual.world_restore_current_calendar_tuple_word_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_current_calendar_tuple_word_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_current_calendar_tuple_word_raw_u32
));
}
}
if let Some(value) = expected.world_restore_current_calendar_tuple_word_2_raw_u32 {
if actual.world_restore_current_calendar_tuple_word_2_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_current_calendar_tuple_word_2_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_current_calendar_tuple_word_2_raw_u32
));
}
}
if let Some(value) = expected.world_restore_absolute_counter_raw_u32 {
if actual.world_restore_absolute_counter_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_absolute_counter_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_absolute_counter_raw_u32
));
}
}
if let Some(value) = expected.world_restore_absolute_counter_mirror_raw_u32 {
if actual.world_restore_absolute_counter_mirror_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_absolute_counter_mirror_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_absolute_counter_mirror_raw_u32
));
}
}
if let Some(slot) = expected.world_restore_disable_cargo_economy_special_condition_slot {
if actual.world_restore_disable_cargo_economy_special_condition_slot != Some(slot) {
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_slot mismatch: expected {slot}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_slot
));
}
}
if let Some(enabled) =
expected.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save
{
if actual.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save
!= Some(enabled)
{
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_reconstructible_from_save mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save
));
}
}
if let Some(enabled) =
expected.world_restore_disable_cargo_economy_special_condition_write_side_grounded
{
if actual.world_restore_disable_cargo_economy_special_condition_write_side_grounded
!= Some(enabled)
{
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_write_side_grounded mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_write_side_grounded
));
}
}
if let Some(enabled) = expected.world_restore_disable_cargo_economy_special_condition_enabled {
if actual.world_restore_disable_cargo_economy_special_condition_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_disable_cargo_economy_special_condition_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_cargo_economy_special_condition_enabled
));
}
}
if let Some(enabled) = expected.world_restore_use_bio_accelerator_cars_enabled {
if actual.world_restore_use_bio_accelerator_cars_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_use_bio_accelerator_cars_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_use_bio_accelerator_cars_enabled
));
}
}
if let Some(enabled) = expected.world_restore_use_wartime_cargos_enabled {
if actual.world_restore_use_wartime_cargos_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_use_wartime_cargos_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_use_wartime_cargos_enabled
));
}
}
if let Some(enabled) = expected.world_restore_disable_train_crashes_enabled {
if actual.world_restore_disable_train_crashes_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_disable_train_crashes_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_train_crashes_enabled
));
}
}
if let Some(enabled) = expected.world_restore_disable_train_crashes_and_breakdowns_enabled {
if actual.world_restore_disable_train_crashes_and_breakdowns_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_disable_train_crashes_and_breakdowns_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_disable_train_crashes_and_breakdowns_enabled
));
}
}
if let Some(enabled) = expected.world_restore_ai_ignore_territories_at_startup_enabled {
if actual.world_restore_ai_ignore_territories_at_startup_enabled != Some(enabled) {
mismatches.push(format!(
"world_restore_ai_ignore_territories_at_startup_enabled mismatch: expected {enabled}, got {:?}",
actual.world_restore_ai_ignore_territories_at_startup_enabled
));
}
}
if let Some(value) = expected.world_restore_limited_track_building_amount {
if actual.world_restore_limited_track_building_amount != Some(value) {
mismatches.push(format!(
"world_restore_limited_track_building_amount mismatch: expected {value}, got {:?}",
actual.world_restore_limited_track_building_amount
));
}
}
if let Some(code) = expected.world_restore_economic_status_code {
if actual.world_restore_economic_status_code != Some(code) {
mismatches.push(format!(
"world_restore_economic_status_code mismatch: expected {code}, got {:?}",
actual.world_restore_economic_status_code
));
}
}
if let Some(value) = expected.world_restore_territory_access_cost {
if actual.world_restore_territory_access_cost != Some(value) {
mismatches.push(format!(
"world_restore_territory_access_cost mismatch: expected {value}, got {:?}",
actual.world_restore_territory_access_cost
));
}
}
if let Some(value) = expected.world_restore_issue_37_value {
if actual.world_restore_issue_37_value != Some(value) {
mismatches.push(format!(
"world_restore_issue_37_value mismatch: expected {value}, got {:?}",
actual.world_restore_issue_37_value
));
}
}
if let Some(value) = expected.world_restore_issue_38_value {
if actual.world_restore_issue_38_value != Some(value) {
mismatches.push(format!(
"world_restore_issue_38_value mismatch: expected {value}, got {:?}",
actual.world_restore_issue_38_value
));
}
}
if let Some(value) = expected.world_restore_issue_39_value {
if actual.world_restore_issue_39_value != Some(value) {
mismatches.push(format!(
"world_restore_issue_39_value mismatch: expected {value}, got {:?}",
actual.world_restore_issue_39_value
));
}
}
if let Some(value) = expected.world_restore_issue_3a_value {
if actual.world_restore_issue_3a_value != Some(value) {
mismatches.push(format!(
"world_restore_issue_3a_value mismatch: expected {value}, got {:?}",
actual.world_restore_issue_3a_value
));
}
}
if let Some(value) = expected.world_restore_issue_37_multiplier_raw_u32 {
if actual.world_restore_issue_37_multiplier_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_issue_37_multiplier_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_issue_37_multiplier_raw_u32
));
}
}
if let Some(value) = &expected.world_restore_issue_37_multiplier_value_f32_text {
if actual
.world_restore_issue_37_multiplier_value_f32_text
.as_ref()
!= Some(value)
{
mismatches.push(format!(
"world_restore_issue_37_multiplier_value_f32_text mismatch: expected {value:?}, got {:?}",
actual.world_restore_issue_37_multiplier_value_f32_text
));
}
}
if let Some(count) = expected.world_restore_finance_neighborhood_count {
if actual.world_restore_finance_neighborhood_count != count {
mismatches.push(format!(
"world_restore_finance_neighborhood_count mismatch: expected {count}, got {}",
actual.world_restore_finance_neighborhood_count
));
}
}
if let Some(labels) = &expected.world_restore_finance_neighborhood_labels {
if &actual.world_restore_finance_neighborhood_labels != labels {
mismatches.push(format!(
"world_restore_finance_neighborhood_labels mismatch: expected {labels:?}, got {:?}",
actual.world_restore_finance_neighborhood_labels
));
}
}
if let Some(value) = expected.world_restore_economic_tuning_mirror_raw_u32 {
if actual.world_restore_economic_tuning_mirror_raw_u32 != Some(value) {
mismatches.push(format!(
"world_restore_economic_tuning_mirror_raw_u32 mismatch: expected {value}, got {:?}",
actual.world_restore_economic_tuning_mirror_raw_u32
));
}
}
if let Some(value) = &expected.world_restore_economic_tuning_mirror_value_f32_text {
if actual
.world_restore_economic_tuning_mirror_value_f32_text
.as_ref()
!= Some(value)
{
mismatches.push(format!(
"world_restore_economic_tuning_mirror_value_f32_text mismatch: expected {value:?}, got {:?}",
actual.world_restore_economic_tuning_mirror_value_f32_text
));
}
}
if let Some(count) = expected.world_restore_economic_tuning_lane_count {
if actual.world_restore_economic_tuning_lane_count != count {
mismatches.push(format!(
"world_restore_economic_tuning_lane_count mismatch: expected {count}, got {}",
actual.world_restore_economic_tuning_lane_count
));
}
}
if let Some(values) = &expected.world_restore_economic_tuning_lane_value_f32_text {
if &actual.world_restore_economic_tuning_lane_value_f32_text != values {
mismatches.push(format!(
"world_restore_economic_tuning_lane_value_f32_text mismatch: expected {values:?}, got {:?}",
actual.world_restore_economic_tuning_lane_value_f32_text
));
}
}
if let Some(kind) = &expected.world_restore_absolute_counter_restore_kind {
if actual.world_restore_absolute_counter_restore_kind.as_ref() != Some(kind) {
mismatches.push(format!(
"world_restore_absolute_counter_restore_kind mismatch: expected {kind:?}, got {:?}",
actual.world_restore_absolute_counter_restore_kind
));
}
}
if let Some(context) = &expected.world_restore_absolute_counter_adjustment_context {
if actual
.world_restore_absolute_counter_adjustment_context
.as_ref()
!= Some(context)
{
mismatches.push(format!(
"world_restore_absolute_counter_adjustment_context mismatch: expected {context:?}, got {:?}",
actual.world_restore_absolute_counter_adjustment_context
));
}
}
if let Some(count) = expected.metadata_count {
if actual.metadata_count != count {
mismatches.push(format!(
"metadata_count mismatch: expected {count}, got {}",
actual.metadata_count
));
}
}
if let Some(count) = expected.company_count {
if actual.company_count != count {
mismatches.push(format!(
"company_count mismatch: expected {count}, got {}",
actual.company_count
));
}
}
if let Some(count) = expected.active_company_count {
if actual.active_company_count != count {
mismatches.push(format!(
"active_company_count mismatch: expected {count}, got {}",
actual.active_company_count
));
}
}
if let Some(count) = expected.company_market_state_owner_count {
if actual.company_market_state_owner_count != count {
mismatches.push(format!(
"company_market_state_owner_count mismatch: expected {count}, got {}",
actual.company_market_state_owner_count
));
}
}
}

View file

@ -0,0 +1,112 @@
use super::*;
use crate::load_fixture_document_from_str;
use rrt_runtime::summary::RuntimeSummary;
const FIXTURE_JSON: &str = r#"
{
"format_version": 1,
"fixture_id": "minimal-world-step-smoke",
"source": {
"kind": "synthetic",
"description": "basic milestone parser smoke fixture"
},
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 0
},
"world_flags": {
"sandbox": false
},
"companies": [
{
"company_id": 1,
"current_cash": 250000,
"debt": 0
}
],
"event_runtime_records": [],
"service_state": {
"periodic_boundary_calls": 0,
"trigger_dispatch_counts": {},
"total_event_record_services": 0,
"dirty_rerun_count": 0
}
},
"commands": [
{
"kind": "advance_to",
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 2
}
}
],
"expected_summary": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 2
},
"world_flag_count": 1,
"company_count": 1,
"event_runtime_record_count": 0,
"world_restore_economic_tuning_lane_count": 0,
"total_company_cash": 250000
}
}
"#;
#[test]
fn parses_and_validates_fixture() {
let fixture = load_fixture_document_from_str(FIXTURE_JSON).expect("fixture should parse");
let report = validate_fixture_document(&fixture);
assert!(report.valid, "report should be valid: {:?}", report.issues);
assert_eq!(fixture.state_origin, FixtureStateOrigin::Inline);
}
#[test]
fn compares_expected_summary() {
let fixture = load_fixture_document_from_str(FIXTURE_JSON).expect("fixture should parse");
let summary = RuntimeSummary::from_state(&fixture.state);
let mismatches = fixture.expected_summary.compare(&summary);
assert_eq!(mismatches.len(), 1);
assert!(mismatches[0].contains("calendar mismatch"));
}
#[test]
fn compares_expected_state_fragment_recursively() {
let expected = serde_json::json!({
"world_flags": {
"sandbox": false
},
"companies": [
{
"company_id": 1
}
]
});
let actual = serde_json::json!({
"world_flags": {
"sandbox": false,
"runtime.effect_fired": true
},
"companies": [
{
"company_id": 1,
"current_cash": 250000
}
]
});
let mismatches = compare_expected_state_fragment(&expected, &actual);
assert!(
mismatches.is_empty(),
"unexpected mismatches: {mismatches:?}"
);
}

Some files were not shown because too many files have changed in this diff Show more