Refactor runtime ownership and clean up warnings
This commit is contained in:
parent
f23a3b3add
commit
486b061558
628 changed files with 97954 additions and 90763 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -4,9 +4,9 @@
|
|||
/ghidra_projects/
|
||||
/rt3_wineprefix/
|
||||
/rt3_wineprefix2/
|
||||
/artifacts/tmp/
|
||||
/tools/py/__pycache__/
|
||||
/.codex
|
||||
/\ %s
|
||||
*.gpr
|
||||
*.rep
|
||||
*.rzdb
|
||||
|
|
|
|||
332
README.md
332
README.md
|
|
@ -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.
|
||||
|
|
|
|||
14
artifacts/captures/README.md
Normal file
14
artifacts/captures/README.md
Normal 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.
|
||||
49
artifacts/exports/rt3-1.06/README.md
Normal file
49
artifacts/exports/rt3-1.06/README.md
Normal 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
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
20
crates/rrt-cli/src/app/command/finance.rs
Normal file
20
crates/rrt-cli/src/app/command/finance.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
158
crates/rrt-cli/src/app/command/mod.rs
Normal file
158
crates/rrt-cli/src/app/command/mod.rs
Normal 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, ¤t_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"),
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
194
crates/rrt-cli/src/app/command/model.rs
Normal file
194
crates/rrt-cli/src/app/command/model.rs
Normal 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 },
|
||||
}
|
||||
66
crates/rrt-cli/src/app/command/runtime/compare.rs
Normal file
66
crates/rrt-cli/src/app/command/runtime/compare.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
73
crates/rrt-cli/src/app/command/runtime/fixture_state.rs
Normal file
73
crates/rrt-cli/src/app/command/runtime/fixture_state.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
146
crates/rrt-cli/src/app/command/runtime/inspect.rs
Normal file
146
crates/rrt-cli/src/app/command/runtime/inspect.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
74
crates/rrt-cli/src/app/command/runtime/mod.rs
Normal file
74
crates/rrt-cli/src/app/command/runtime/mod.rs
Normal 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))
|
||||
}
|
||||
44
crates/rrt-cli/src/app/command/runtime/scan.rs
Normal file
44
crates/rrt-cli/src/app/command/runtime/scan.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
18
crates/rrt-cli/src/app/command/validate.rs
Normal file
18
crates/rrt-cli/src/app/command/validate.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
12
crates/rrt-cli/src/app/dispatch/finance.rs
Normal file
12
crates/rrt-cli/src/app/dispatch/finance.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
81
crates/rrt-cli/src/app/dispatch/mod.rs
Normal file
81
crates/rrt-cli/src/app/dispatch/mod.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
30
crates/rrt-cli/src/app/dispatch/runtime/compare.rs
Normal file
30
crates/rrt-cli/src/app/dispatch/runtime/compare.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
43
crates/rrt-cli/src/app/dispatch/runtime/fixture_state.rs
Normal file
43
crates/rrt-cli/src/app/dispatch/runtime/fixture_state.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
85
crates/rrt-cli/src/app/dispatch/runtime/inspect.rs
Normal file
85
crates/rrt-cli/src/app/dispatch/runtime/inspect.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
15
crates/rrt-cli/src/app/dispatch/runtime/mod.rs
Normal file
15
crates/rrt-cli/src/app/dispatch/runtime/mod.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
28
crates/rrt-cli/src/app/dispatch/runtime/scan.rs
Normal file
28
crates/rrt-cli/src/app/dispatch/runtime/scan.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
15
crates/rrt-cli/src/app/dispatch/validate.rs
Normal file
15
crates/rrt-cli/src/app/dispatch/validate.rs
Normal 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(())
|
||||
}
|
||||
110
crates/rrt-cli/src/app/finance.rs
Normal file
110
crates/rrt-cli/src/app/finance.rs
Normal 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(),
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
446
crates/rrt-cli/src/app/helpers/inspect.rs
Normal file
446
crates/rrt-cli/src/app/helpers/inspect.rs
Normal 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)
|
||||
)
|
||||
}
|
||||
2
crates/rrt-cli/src/app/helpers/mod.rs
Normal file
2
crates/rrt-cli/src/app/helpers/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub(super) mod inspect;
|
||||
pub(super) mod state_io;
|
||||
102
crates/rrt-cli/src/app/helpers/state_io.rs
Normal file
102
crates/rrt-cli/src/app/helpers/state_io.rs
Normal 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(())
|
||||
}
|
||||
16
crates/rrt-cli/src/app/mod.rs
Normal file
16
crates/rrt-cli/src/app/mod.rs
Normal 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()
|
||||
}
|
||||
258
crates/rrt-cli/src/app/reports/inspect.rs
Normal file
258
crates/rrt-cli/src/app/reports/inspect.rs
Normal 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,
|
||||
}
|
||||
2
crates/rrt-cli/src/app/reports/mod.rs
Normal file
2
crates/rrt-cli/src/app/reports/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub(super) mod inspect;
|
||||
pub(super) mod state;
|
||||
72
crates/rrt-cli/src/app/reports/state.rs
Normal file
72
crates/rrt-cli/src/app/reports/state.rs
Normal 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,
|
||||
}
|
||||
472
crates/rrt-cli/src/app/runtime_compare/candidate_table.rs
Normal file
472
crates/rrt-cli/src/app/runtime_compare/candidate_table.rs
Normal 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
|
||||
}
|
||||
104
crates/rrt-cli/src/app/runtime_compare/common.rs
Normal file
104
crates/rrt-cli/src/app/runtime_compare/common.rs
Normal 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(),
|
||||
});
|
||||
}
|
||||
43
crates/rrt-cli/src/app/runtime_compare/mod.rs
Normal file
43
crates/rrt-cli/src/app/runtime_compare/mod.rs
Normal 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,
|
||||
};
|
||||
94
crates/rrt-cli/src/app/runtime_compare/post_special.rs
Normal file
94
crates/rrt-cli/src/app/runtime_compare/post_special.rs
Normal 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)
|
||||
}
|
||||
195
crates/rrt-cli/src/app/runtime_compare/profiles.rs
Normal file
195
crates/rrt-cli/src/app/runtime_compare/profiles.rs
Normal 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)
|
||||
}
|
||||
250
crates/rrt-cli/src/app/runtime_compare/recipe_book.rs
Normal file
250
crates/rrt-cli/src/app/runtime_compare/recipe_book.rs
Normal 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(¤t).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()
|
||||
}
|
||||
33
crates/rrt-cli/src/app/runtime_compare/region.rs
Normal file
33
crates/rrt-cli/src/app/runtime_compare/region.rs
Normal 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(())
|
||||
}
|
||||
345
crates/rrt-cli/src/app/runtime_compare/setup_payload.rs
Normal file
345
crates/rrt-cli/src/app/runtime_compare/setup_payload.rs
Normal 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)
|
||||
}
|
||||
115
crates/rrt-cli/src/app/runtime_fixture_state/fixtures.rs
Normal file
115
crates/rrt-cli/src/app/runtime_fixture_state/fixtures.rs
Normal 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(())
|
||||
}
|
||||
9
crates/rrt-cli/src/app/runtime_fixture_state/mod.rs
Normal file
9
crates/rrt-cli/src/app/runtime_fixture_state/mod.rs
Normal 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};
|
||||
70
crates/rrt-cli/src/app/runtime_fixture_state/save_import.rs
Normal file
70
crates/rrt-cli/src/app/runtime_fixture_state/save_import.rs
Normal 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(())
|
||||
}
|
||||
29
crates/rrt-cli/src/app/runtime_fixture_state/save_load.rs
Normal file
29
crates/rrt-cli/src/app/runtime_fixture_state/save_load.rs
Normal 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(())
|
||||
}
|
||||
81
crates/rrt-cli/src/app/runtime_fixture_state/state.rs
Normal file
81
crates/rrt-cli/src/app/runtime_fixture_state/state.rs
Normal 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(())
|
||||
}
|
||||
175
crates/rrt-cli/src/app/runtime_inspect/assets.rs
Normal file
175
crates/rrt-cli/src/app/runtime_inspect/assets.rs
Normal 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(())
|
||||
}
|
||||
128
crates/rrt-cli/src/app/runtime_inspect/maps.rs
Normal file
128
crates/rrt-cli/src/app/runtime_inspect/maps.rs
Normal 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(())
|
||||
}
|
||||
19
crates/rrt-cli/src/app/runtime_inspect/mod.rs
Normal file
19
crates/rrt-cli/src/app/runtime_inspect/mod.rs
Normal 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,
|
||||
};
|
||||
117
crates/rrt-cli/src/app/runtime_inspect/smp.rs
Normal file
117
crates/rrt-cli/src/app/runtime_inspect/smp.rs
Normal 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(())
|
||||
}
|
||||
245
crates/rrt-cli/src/app/runtime_scan/aligned_band.rs
Normal file
245
crates/rrt-cli/src/app/runtime_scan/aligned_band.rs
Normal 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(¤t).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,
|
||||
})
|
||||
}
|
||||
388
crates/rrt-cli/src/app/runtime_scan/candidate_table.rs
Normal file
388
crates/rrt-cli/src/app/runtime_scan/candidate_table.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
130
crates/rrt-cli/src/app/runtime_scan/common.rs
Normal file
130
crates/rrt-cli/src/app/runtime_scan/common.rs
Normal 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
|
||||
}
|
||||
}
|
||||
15
crates/rrt-cli/src/app/runtime_scan/mod.rs
Normal file
15
crates/rrt-cli/src/app/runtime_scan/mod.rs
Normal 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;
|
||||
489
crates/rrt-cli/src/app/runtime_scan/post_special.rs
Normal file
489
crates/rrt-cli/src/app/runtime_scan/post_special.rs
Normal 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(¤t).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()
|
||||
}
|
||||
183
crates/rrt-cli/src/app/runtime_scan/recipe_book.rs
Normal file
183
crates/rrt-cli/src/app/runtime_scan/recipe_book.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
167
crates/rrt-cli/src/app/runtime_scan/special_conditions.rs
Normal file
167
crates/rrt-cli/src/app/runtime_scan/special_conditions.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
581
crates/rrt-cli/src/app/tests/compare.rs
Normal file
581
crates/rrt-cli/src/app/tests/compare.rs
Normal 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")
|
||||
);
|
||||
}
|
||||
32
crates/rrt-cli/src/app/tests/mod.rs
Normal file
32
crates/rrt-cli/src/app/tests/mod.rs
Normal 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::*;
|
||||
410
crates/rrt-cli/src/app/tests/state/diff.rs
Normal file
410
crates/rrt-cli/src/app/tests/state/diff.rs
Normal 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);
|
||||
}
|
||||
176
crates/rrt-cli/src/app/tests/state/document_io.rs
Normal file
176
crates/rrt-cli/src/app/tests/state/document_io.rs
Normal 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);
|
||||
}
|
||||
77
crates/rrt-cli/src/app/tests/state/fixture_summary.rs
Normal file
77
crates/rrt-cli/src/app/tests/state/fixture_summary.rs
Normal 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");
|
||||
}
|
||||
7
crates/rrt-cli/src/app/tests/state/mod.rs
Normal file
7
crates/rrt-cli/src/app/tests/state/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use super::*;
|
||||
|
||||
mod diff;
|
||||
mod document_io;
|
||||
mod fixture_summary;
|
||||
mod save_slice_overlay;
|
||||
mod snapshot_io;
|
||||
265
crates/rrt-cli/src/app/tests/state/save_slice_overlay.rs
Normal file
265
crates/rrt-cli/src/app/tests/state/save_slice_overlay.rs
Normal 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"
|
||||
}));
|
||||
}
|
||||
18
crates/rrt-cli/src/app/tests/state/snapshot_io.rs
Normal file
18
crates/rrt-cli/src/app/tests/state/snapshot_io.rs
Normal 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);
|
||||
}
|
||||
6
crates/rrt-cli/src/app/tests/support/json.rs
Normal file
6
crates/rrt-cli/src/app/tests/support/json.rs
Normal 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:?}"
|
||||
);
|
||||
}
|
||||
5
crates/rrt-cli/src/app/tests/support/mod.rs
Normal file
5
crates/rrt-cli/src/app/tests/support/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod json;
|
||||
mod temp_files;
|
||||
|
||||
pub(crate) use json::*;
|
||||
pub(crate) use temp_files::*;
|
||||
15
crates/rrt-cli/src/app/tests/support/temp_files.rs
Normal file
15
crates/rrt-cli/src/app/tests/support/temp_files.rs
Normal 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
|
||||
}
|
||||
119
crates/rrt-cli/src/app/validate.rs
Normal file
119
crates/rrt-cli/src/app/validate.rs
Normal 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
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
68
crates/rrt-fixtures/src/schema/document.rs
Normal file
68
crates/rrt-fixtures/src/schema/document.rs
Normal 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>,
|
||||
}
|
||||
17
crates/rrt-fixtures/src/schema/mod.rs
Normal file
17
crates/rrt-fixtures/src/schema/mod.rs
Normal 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;
|
||||
49
crates/rrt-fixtures/src/schema/state_fragment.rs
Normal file
49
crates/rrt-fixtures/src/schema/state_fragment.rs
Normal 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:?}"
|
||||
)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
302
crates/rrt-fixtures/src/schema/summary.rs
Normal file
302
crates/rrt-fixtures/src/schema/summary.rs
Normal 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>,
|
||||
}
|
||||
345
crates/rrt-fixtures/src/schema/summary_compare/collections.rs
Normal file
345
crates/rrt-fixtures/src/schema/summary_compare/collections.rs
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
22
crates/rrt-fixtures/src/schema/summary_compare/mod.rs
Normal file
22
crates/rrt-fixtures/src/schema/summary_compare/mod.rs
Normal 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
|
||||
}
|
||||
}
|
||||
314
crates/rrt-fixtures/src/schema/summary_compare/packed_events.rs
Normal file
314
crates/rrt-fixtures/src/schema/summary_compare/packed_events.rs
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
398
crates/rrt-fixtures/src/schema/summary_compare/world.rs
Normal file
398
crates/rrt-fixtures/src/schema/summary_compare/world.rs
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
112
crates/rrt-fixtures/src/schema/tests.rs
Normal file
112
crates/rrt-fixtures/src/schema/tests.rs
Normal 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
Loading…
Add table
Add a link
Reference in a new issue